Understand how pg_mooncake, pg_duckdb, duckdb_mooncake, and moonlink coordinate writes, reads, and Iceberg snapshots.
This is the multi-page printable view of this section. .
Welcome to pg_mooncake
- 1: Architecture
- 2: Moonlink Ingestion Engine
- 3: duckdb_mooncake & DuckDB 2.0 Integration
- 4: v0.1 vs v0.2 & v0.3 Evolution
pg_mooncake is a PostgreSQL extension that adds real-time columnstore mirror tables (USING mooncake), DuckDB 2.0 vectorized analytical execution (via pg_duckdb and duckdb_mooncake), and open Apache Iceberg v2 storage (powered by moonlink) directly inside PostgreSQL.
PostgreSQL with pg_mooncake ranks among the fastest analytical databases on ClickBench while keeping your existing PostgreSQL transactional write path untouched. Originally created by Mooncake Labs, the extension suite (pg_mooncake v0.3.0, duckdb_mooncake v0.0.2, and moonlink v0.0.1) is now maintained by the PGSTY community for PostgreSQL 14–18.
v0.3.0)pgsty/pg_mooncake(v0.3.0,MIT): PostgreSQL extension providing theUSING mooncaketable access method,mooncake.*SQL procedures, and the embedded Moonlink background worker (pgrx 0.19.3).pgsty/moonlink(v0.0.1,BSL-1.1withpg_mooncakeLinking Exception): Real-time Rust storage engine that ingests PostgreSQL logical replication streams into in-memory Arrow buffers, Roaring Bitmap deletion vectors, and Apache Iceberg v2 / Parquet files.pgsty/duckdb_mooncake(v0.0.2,MIT): DuckDB 2.0 storage extension bundled withpg_mooncakethat scans Moonlink tables with multi-file Parquet statistics pruning.
The Problem
PostgreSQL is an exceptional transactional (OLTP) database, but its row-oriented heap storage and tuple-at-a-time executor are not designed for large-scale analytical scans (OLAP). Historically, PostgreSQL teams faced two flawed choices:
- Adopt a separate analytical system (ClickHouse, Snowflake, BigQuery) Requires maintaining fragile CDC pipelines (Debezium + Kafka), duplicating data models, dealing with eventual consistency lag, and losing the ability to join live operational tables with analytical history in a single query.
- Use in-database columnstores without vectorized execution or real-time CDC
Traditional columnar extensions either lack a modern vectorized execution engine or cannot handle high-frequency transactional
INSERT,UPDATE, andDELETEoperations without generating thousands of tiny Parquet files.

How pg_mooncake Solves It
Starting in v0.2 and refined in PGSTY’s v0.3.0 release, pg_mooncake introduces a columnstore mirror architecture:
- Write to Rowstore, Read from Columnstore: Your application continues writing (
INSERT,UPDATE,DELETE) to the standard PostgreSQLtradesheap table with full ACID guarantees and indexes. - Sub-Second Logical Replication via Moonlink: Behind the scenes,
pg_mooncake’s background worker streams WAL changes intomoonlink, buffering recent mutations in memory as Apache Arrow batches and tracking updates/deletes via Roaring Bitmap deletion vectors before compacting into Parquet files. - Vectorized DuckDB 2.0 Queries: Analytical queries against
trades_analyticsare routed throughpg_duckdb 1.2.0andduckdb_mooncake v0.0.2, merging in-memory Arrow buffers and on-disk Parquet segments with segment elimination and LSN consistency barriers. - Open Apache Iceberg v2 Lakehouse: With
CALL mooncake.create_snapshot('trades_analytics'), Moonlink emits standard Apache Iceberg v2 metadata on local disk, AWS S3, or Cloudflare R2—ready for Spark, Snowflake, DuckDB, or PyIceberg without any ETL pipeline.
Explore This Section
Deep dive into Moonlink (v0.0.1), in-memory Arrow write buffers, deletion vectors, and standalone HTTP/RPC server deployment.
Learn how duckdb_mooncake v0.0.2 integrates with DuckDB 2.0 and pg_duckdb 1.2.0 for vectorized scans and Parquet statistics pruning.
Compare the legacy v0.1 Delta Lake (USING columnstore) architecture with v0.2 Moonlink mirrors and PGSTY’s v0.3.0 enhancements.
1 - Architecture
pg_mooncake (v0.3.0) combines three specialized components inside a single PostgreSQL deployment:
pg_duckdb(v1.2.0): Embeds the DuckDBv2.0.0vectorized analytical execution engine inside PostgreSQL and routes queries against non-heap table access methods.moonlink(v0.0.1): A Rust library and background worker embedded insidepg_mooncakethat ingests PostgreSQL logical replication streams, maintains an in-memory Apache Arrow write buffer and Roaring Bitmap deletion vectors, flushes Parquet files, and emits Apache Iceberg v2 table metadata.duckdb_mooncake(v0.0.2): A DuckDB 2.0 C++/Rust storage extension loaded automatically bypg_mooncakefrom a local extension repository (mooncake.extension_repository). It connects to Moonlink over a local Unix domain socket RPC (pg_mooncake/moonlink.sock) to read consistent snapshots across both flushed Parquet files and buffered Arrow batches.
Together, they expose a native PostgreSQL Table Access Method (USING mooncake) managed through CALL mooncake.create_table(dst, src).

Component Pinning in pg_mooncake 0.3.0
| Component | Version / Tag | Pinned Commit (versions.json) |
Role |
|---|---|---|---|
pg_mooncake |
v0.3.0 |
1a9cbfd65be9a6486d020120acb49ef0c6800d68 |
PostgreSQL extension (pgrx 0.19.3), SQL procedures, USING mooncake TAM, and background worker launcher |
moonlink |
v0.0.1 |
b298ef7444890e3c8af5544e350232518bcfd8e5 |
Embedded Rust CDC ingestion engine, Arrow buffer, deletion vectors, and Iceberg v2 snapshot writer |
duckdb_mooncake |
v0.0.2 |
49765a369ad88986916ce53c969ed2ce4ba66fac |
DuckDB 2.0 extension (mooncake.duckdb_extension) scanning Moonlink tables and Parquet statistics |
pg_duckdb |
v1.2.0 |
8756099e267c47f0b86c24de4cf995327f1f4279 |
Embedded DuckDB execution bridge inside PostgreSQL (enforced by SQL installation guard) |
duckdb |
v2.0.0 |
d54cbbe3df17e8e5ce68312f483b4155836112aa |
Underlying C++ vectorized analytical query engine |
Write Path
In pg_mooncake, you never write directly to the USING mooncake columnstore table. Instead, you write to your regular PostgreSQL rowstore (heap or partitioned) table:
- Initial Table Copy & Slot Creation: When you execute
CALL mooncake.create_table('dst', 'src'),pg_mooncake:- Inspects the source table’s schema and creates the destination table with
USING mooncake. - Sets
REPLICA IDENTITY FULLon the source table (and, inv0.3.0, automatically on all existing leaf partitions if the source is a partitioned table) if the source table does not have a primary key. - Creates a PostgreSQL publication and logical replication slot (
pgoutput), copies existing rows fromsrcinto Moonlink, and records the starting WAL LSN.
- Inspects the source table’s schema and creates the destination table with
- Continuous Logical Replication: A background worker (
mooncake_bgworker) continuously consumes WAL changes (INSERT,UPDATE,DELETE) from the logical replication slot and streams them tomoonlink. - Buffered Arrow + Deletion Vectors:
- Inserts are appended to an in-memory Apache Arrow write buffer (
write_buffer_size, default32 MB), making new rows immediately visible to analytical queries without writing a tiny Parquet file per transaction. - Deletes record the target row’s location in an in-memory/on-disk Roaring Bitmap deletion vector, avoiding expensive full-file Parquet rewrites on every
DELETE. - Updates are processed atomically as a deletion vector entry for the old tuple plus an append to the Arrow buffer for the new tuple.
- Inserts are appended to an in-memory Apache Arrow write buffer (
- Background Compaction & Parquet Flush: Once the write buffer reaches its threshold or when
CALL mooncake.create_snapshot('dst')/CALL mooncake.optimize_table('dst', 'full')is invoked, Moonlink flushes the buffered Arrow records into compressed Parquet files and updates the Apache Iceberg v2 metadata.
Read Path
When you run a SELECT query that references a USING mooncake columnstore table:
- Query Routing via
pg_duckdb: PostgreSQL’s planner recognizes themooncaketable access method (registered withpg_duckdbviaRegisterDuckdbTableAm("mooncake")) and hands the query plan over to the embedded DuckDB 2.0 engine. - Automatic Extension Attachment: On the first scan in a backend session,
pg_mooncakeconfiguresSET extension_directory = '<mooncake.extension_repository>', loads themooncakeDuckDB extension (duckdb_mooncake v0.0.2), and executesATTACH '' AS mooncake (TYPE mooncake, METADATA '<pgdata>/pg_mooncake'). - LSN Synchronization Barrier: Before scanning a table,
duckdb_mooncakequeries PostgreSQL’s current WAL write/insert LSN and instructs Moonlink via RPC (scan_table_begin) to wait until the replication stream has ingested up to that LSN—guaranteeing read-your-own-writes transactional consistency. - Hybrid Arrow + Parquet Scan with Segment Elimination:
- Moonlink returns the list of active Parquet data files, active deletion vectors, and a pointer to the current in-memory Arrow buffer.
duckdb_mooncakereads Parquet column min/max statistics through DuckDB 2.0’s exported multi-file reader interface to skip irrelevant files and row groups (segment elimination), applies deletion vectors, and streams the combined results back to PostgreSQL.
Storage Layout
By default (mooncake.default_bucket = ''), Moonlink stores table metadata, deletion vectors, and Parquet data files under your PostgreSQL data directory at $PGDATA/pg_mooncake/.
When mooncake.default_bucket is set to an s3:// or r2:// URI (with credentials configured in duckdb.secrets), Moonlink writes Parquet data files and Apache Iceberg v2 metadata (metadata/*.metadata.json and Avro manifest lists) directly to object storage. You can inspect the active warehouse path and LSN progress for every mirrored table at any time with:
2 - Moonlink Ingestion Engine
Moonlink (pgsty/moonlink, current release v0.0.1) is a Rust library and standalone service designed to write and maintain real-time Apache Iceberg v2 and Delta Lake tables for operational systems and event streams without Apache Spark or Kafka.
Inside pg_mooncake, Moonlink runs as an embedded PostgreSQL background worker (mooncake_bgworker) communicating over a local Unix domain socket ($PGDATA/pg_mooncake/moonlink.sock). It can also be compiled and deployed as a standalone HTTP REST and gRPC ingestion server (moonlink_service).
BSL-1.1 with pg_mooncake Linking Exception)The core moonlink server engine is licensed under BSL-1.1 (converting automatically to Apache-2.0 on 2029-05-05) with an explicit PG_MOONCAKE LINKING & DISTRIBUTION EXCEPTION granting unrestricted linking, modification, and redistribution when embedded in or distributed as part of pg_mooncake. Its protocol and client crates (moonlink_rpc, moonlink_proto, moonlink_error, moonlink_table_metadata) are licensed under MIT. See Licensing for full text.
Core Design Principles
Traditional data lake writers face a fundamental trade-off between data freshness and query performance: flushing every row mutation immediately creates thousands of tiny Parquet files, whereas batching writes for 5–15 minutes introduces unacceptable latency for operational analytics.
Moonlink resolves this trade-off with a multi-tiered storage architecture:
- In-Memory Apache Arrow Write Buffer: Incoming
INSERTandUPDATErows are appended to an active in-memory ArrowRecordBatchbuffer (write_buffer_size, default32 MB). Readers (duckdb_mooncake) can scan this buffer directly with zero-copy Arrow integration before it is ever flushed to Parquet. - Equality & Position Deletion Vectors (Roaring Bitmaps): Instead of rewriting Parquet files on every
UPDATEorDELETE, Moonlink indexes primary/replica keys via a B-tree index (moonlink_btree) and marks deleted row offsets in compressed Roaring Bitmaps. - LSN-Tracked Commits & Non-Blocking Snapshots: Every mutation ingested from PostgreSQL WAL is tagged with its commit
lsn. Readers pass their target LSN toscan_table_begin, waiting only if the replication stream has not yet reached that LSN. - Asynchronous Compaction & Iceberg v2 Metadata: Background compaction merges flushed Parquet segments, reconciles deletion vectors, and writes standard Apache Iceberg v2
metadata.jsonand Avro manifest files to local disk or S3/R2 object storage.
What’s Fixed in moonlink v0.0.1
In PGSTY’s moonlink v0.0.1 (b298ef7444890e3c8af5544e350232518bcfd8e5) release:
- Non-blocking
create_snapshot(lsn)under unrelated WAL progress: Previously, callingCALL mooncake.create_snapshot('table')when the PostgreSQL database WAL had advanced due to writes on unrelated tables caused Moonlink’s snapshot waiter to hang indefinitely waiting for a table-level commit at the latest database LSN. Inv0.0.1, replication progress notifications advance the table’s safe LSN watermark and flush older committed data immediately, even when no new commits arrive for that specific table. - 58/58 Table Handler Unit Tests: Verified across append, update, delete, rollback, compaction, and snapshot barrier scenarios on Rocky Linux 10 (
aarch64) and Ubuntu 24.04 (aarch64). - Security Dependency Audit Updates: Patched
bytes,crossbeam-epoch,h2,lz4_flex,quinn-proto,rustls,rustls-webpki,webpki-roots,time,zstd,anyhow,event-listener,lru(0.18.5),memmap2,rand,tokio(1.51.6),aws-lc-rs, andserial_test.
Standalone Deployment (moonlink_service)
While pg_mooncake embeds Moonlink automatically, you can also run moonlink_service as a standalone ingestion server for direct HTTP REST or gRPC streaming into Iceberg tables.
Building moonlink_service from Source
Standalone REST API Workflow
The standalone service listens on port 3030 by default and exposes JSON endpoints for table creation, row ingestion, and Iceberg snapshot generation:
Querying the Iceberg Snapshot from DuckDB
Once a snapshot is created, any Iceberg-compatible engine (DuckDB, Spark, Trino, Snowflake, PyIceberg) can read the table directly:
3 - duckdb_mooncake & DuckDB 2.0 Integration
duckdb_mooncake (pgsty/duckdb_mooncake, current release v0.0.2, MIT license) is a C++ and Rust FFI storage extension for DuckDB v2.0.0. It allows DuckDB (both standalone and embedded inside PostgreSQL via pg_duckdb 1.2.0) to attach directly to a moonlink metadata directory and execute vectorized scans over Moonlink tables.
Role Inside pg_mooncake 0.3.0
When pg_mooncake executes a query against a USING mooncake table inside PostgreSQL, pg_duckdb delegates the table scan to DuckDB. pg_mooncake automatically initializes the DuckDB session by running:
Local Extension Repository (mooncake.extension_repository)
Rather than downloading an unsigned binary from a remote community S3 bucket at runtime, pg_mooncake 0.3.0 builds and packages mooncake.duckdb_extension (v0.0.2) directly alongside the PostgreSQL extension into a local DuckDB extension repository:
The PostgreSQL GUC mooncake.extension_repository points to this directory (or can be overridden in postgresql.conf when installing to a custom path), ensuring offline, deterministic, byte-for-byte reproducible loading with duckdb.allow_unsigned_extensions = true.
DuckDB 2.0 Adaptation in v0.0.2
In PGSTY’s duckdb_mooncake v0.0.2 (49765a369ad88986916ce53c969ed2ce4ba66fac) release, the extension was ported to DuckDB v2.0.0 (d54cbbe3df17e8e5ce68312f483b4155836112aa) and pg_duckdb 1.2.0:
- DuckDB 2.0 Storage & Catalog APIs: Updated storage extension registration, catalog entry lookup, Arrow schema conversion (
nanoarrow 0.7), table function binding, multi-file lists, and table filter pushdown to match DuckDB 2.0 signatures. - Exported Multi-File Parquet Statistics Interface: Earlier builds depended on internal
ParquetReadersymbols that are no longer exported bylibduckdb.soin DuckDB 2.0.duckdb_mooncake v0.0.2reads column statistics through DuckDB 2.0’s exported multi-file reader interface (MultiFileReader), verifying zero unresolved dynamic symbols (ldd -r) againstpg_duckdb’slibduckdb.so. - Object-Cache Deadlock Elimination: Previously, constructing cached statistics entries inside the object-cache mutex could trigger a reentrant lock deadlock when loading Parquet file metadata also accessed the DuckDB object cache.
v0.0.2constructs statistics entries outside the object-cache mutex before inserting them into the cache. - CRoaring & Corrosion Build Hardening: Links against
Corrosion v0.5,nanoarrow 0.7, andCRoaring v5.5.4(with SHA-256 verification when a systemlibroaringis absent), and builds the Rustmoonlink_fficlient with--lockedCargo dependencies (0RustSec vulnerability matches).
Standalone Usage in DuckDB CLI
You can also load duckdb_mooncake directly in a standalone DuckDB v2.0.0 CLI (duckdb -unsigned) to inspect or query a Moonlink directory:
4 - v0.1 vs v0.2 & v0.3 Evolution
pg_mooncake has evolved through three major architectural milestones:
v0.1(Legacy Delta Lake Engine): Introduced directCREATE TABLE ... USING columnstoretables backed by Delta Lake and DuckDB (pg_duckdb).v0.2(Moonlink & Apache Iceberg Redesign): Replaced direct columnstore writes with a columnstore mirror architecture (CALL mooncake.create_table(dst, src)+USING mooncake) powered bymoonlinklogical replication, in-memory Arrow write buffers, Roaring deletion vectors, and Apache Iceberg v2 metadata.v0.3.0(PGSTY Maintained Release): Upgradedpg_mooncaketopgrx 0.19.3(PostgreSQL 14–18), portedduckdb_mooncake v0.0.2topg_duckdb 1.2.0and DuckDBv2.0.0, fixed Moonlink (v0.0.1) snapshot LSN hangs and partitioned table replica identity handling, and patched 20+ RustSec dependency advisories.
v0.1 Documentation?Complete documentation for the legacy v0.1 release (USING columnstore, mooncake.create_secret, mooncake.columnstore_tables, mooncake.read_parquet) is preserved in the v0.1 Archive (Delta Lake).
Architectural Comparison
| Feature | v0.1 (Legacy) |
v0.2 (Moonlink Redesign) |
v0.3.0 (Current PGSTY Release) |
|---|---|---|---|
| Table Creation | CREATE TABLE t (...) USING columnstore; |
CALL mooncake.create_table(dst, src); |
CALL mooncake.create_table(dst, src, src_uri, table_config); |
| Table Access Method | USING columnstore |
USING mooncake (read-only mirror) |
USING mooncake (read-only mirror) |
| Write Path | Direct DML on columnstore table (1 Parquet file per transaction) | Write to Postgres heap table; continuous logical replication via moonlink |
Write to heap/partitioned table; logical replication via moonlink v0.0.1 |
| Small-Write / Update Handling | Slow for frequent OLTP writes; rewrites Parquet files | Sub-second in-memory Arrow buffer + Roaring Bitmap deletion vectors | Sub-second Arrow buffer + deletion vectors + non-blocking LSN snapshots |
| Open Lakehouse Format | Delta Lake (mooncake.columnstore_tables) |
Apache Iceberg v2 (mooncake.list_tables()) |
Apache Iceberg v2 (mooncake.list_tables()) |
| DuckDB Engine | DuckDB 1.1 / pg_duckdb 0.2 |
DuckDB 1.3 / pg_duckdb |
DuckDB v2.0.0 / pg_duckdb 1.2.0 (duckdb_mooncake v0.0.2) |
| PostgreSQL Versions | PostgreSQL 14–17 (pgrx 0.12) |
PostgreSQL 14–17 (pgrx 0.16.1) |
PostgreSQL 14–18 (pgrx 0.19.3) |
| Partitioned Source Tables | Not supported | Supported (manual REPLICA IDENTITY FULL on partitions) |
Automatically sets REPLICA IDENTITY FULL on existing leaf partitions |
Why v0.2 / v0.3 Moved Away from v0.1’s Write Path
Limitations of v0.1
In v0.1, writing directly to a USING columnstore table worked well for large batch loads (100,000–1,000,000 rows per transaction), but struggled in real-world operational PostgreSQL workloads:
- No indexes or constraints: You could not create primary keys, unique constraints, or B-tree indexes on a
USING columnstoretable. - Small-file amplification: Every
INSERTorUPDATEtransaction created a new Parquet file, quickly degrading scan performance under OLTP traffic. - DuckDB write-path coupling: Routing transactional writes through DuckDB inside PostgreSQL’s executor created memory and transaction-handling bottlenecks.
How v0.2 and v0.3.0 Solve This
By decoupling the OLTP write path (standard PostgreSQL heap tables) from the analytical storage and compute path (moonlink + pg_duckdb 1.2.0 + duckdb_mooncake v0.0.2):
- Zero Impact on OLTP: Your primary tables keep all PostgreSQL features—primary keys, foreign keys, B-tree/GIN/vchord indexes, triggers, and row-level locking.
- High-Throughput Mutations:
moonlinkabsorbs high-frequencyINSERT,UPDATE, andDELETEstreams in memory using Arrow buffers and Roaring deletion vectors, flushing optimized Parquet files in the background. - Schema Evolution:
ALTER TABLE ... ADD COLUMNandDROP COLUMNon the source heap table automatically propagate to the columnstore mirror. - Remote Replication (
src_uri): You can even runpg_mooncakeon a dedicated analytical PostgreSQL instance and mirror tables from a remote production PostgreSQL database (including RDS, Aurora, Supabase, or Neon) viasrc_uri => 'postgresql://...'.