Porting pg_mooncake and duckdb_mooncake to DuckDB 2.0, pg_duckdb 1.2.0, and pgrx 0.19.3
When Mooncake Labs paused public updates to pg_mooncake after v0.2.0, the extension was pinned to an earlier DuckDB 1.3.x / pg_duckdb snapshot and pgrx 0.12. Meanwhile, the PostgreSQL ecosystem moved forward to PostgreSQL 18, pgrx 0.19.3, and pg_duckdb 1.2.0 (which embeds DuckDB v2.0.0).
This article walks through the engineering changes required to bring the entire Mooncake stack — pg_mooncake v0.3.0, duckdb_mooncake v0.0.2, and moonlink v0.0.1 — onto DuckDB 2.0.0, pg_duckdb 1.2.0, and pgrx 0.19.3.
Architecture Recap: How the Three Repositories Fit Together
Understanding the build and runtime boundaries between the three repositories explains why upgrading DuckDB touches both C++ and Rust across multiple crates:
pg_mooncakeis apgrxRust extension that managesmooncake.create_table(dst, src), spawns themooncake_workerbackground process, streams logical replication changes into an embeddedmoonlinkengine, and compilesduckdb_mooncakeinto the same shared library.pg_duckdbembedslibduckdb(v2.0.0) inside PostgreSQL and intercepts queries referencingcolumnstoretables.duckdb_mooncakeregisters a DuckDBStorageExtension(mooncake) that resolves columnstore tables via Moonlink’s UNIX domain socket (mooncake.sock), waits for the target PostgreSQL WAL LSN, and delegates file scans to DuckDB’s parallelparquet_scanreader alongside an in-memory Puffin deletion-vector reader.
1. Adapting duckdb_mooncake to DuckDB 2.0.0 C++ APIs
DuckDB 2.0.0 (d54cbbe3df17e8e5ce68312f483b4155836112aa) introduced several breaking C++ API changes across its catalog, storage extension, and filter pushdown layers:
StorageExtension Registration (StorageExtension::Register)
In DuckDB 1.x, extensions mutated config.storage_extensions["mooncake"] directly. In DuckDB 2.0, storage extensions are registered via StorageExtension::Register(config, "mooncake", make_shared_ptr<MooncakeStorageExtension>()).
TableCatalogEntry::GetColumns() & GetTypes()
MooncakeTableEntry inherits from TableCatalogEntry. We aligned its constructor and column projection helpers with DuckDB 2.0’s ColumnList and LogicalType APIs while preserving the virtual __row_id (UBIGINT) pseudo-column used by Moonlink deletion vectors.
TableFilterSet Iterator Refactoring
In DuckDB 1.x, TableFilterSet exposed a direct std::unordered_map<idx_t, unique_ptr<TableFilter>> filters member. In DuckDB 2.0, TableFilterSet encapsulates its filter map and provides range-based iteration yielding (FilterIndex, const TableFilter &) entries.
While porting MooncakeScanFunction::mooncake_scan_bind to the new iterator API, we uncovered and fixed a subtle bug in IS NULL / IS NOT NULL filter pushdown:
- When a query projected a subset of columns (for example,
SELECT b FROM t_col WHERE a IS NOT NULL AND b > 10),CONJUNCTION_ANDand comparison filters remapped their column index from the scan projection index to the physical Parquet table column index (column_ids[filter_col_idx]), butIS_NULLandIS_NOT_NULLfilters previously passed the unmapped scan index directly to the underlying Parquet filter set. - Under DuckDB 2.0, an out-of-bounds or mismatched filter column index on
parquet_scancaused a segmentation fault (SIGSEGV) duringSELECT count(*) FROM (SELECT DISTINCT c, a, b FROM t WHERE c IS NULL AND b > 2). - Remapping
IS_NULLandIS_NOT_NULLfilter indices throughcolumn_idsresolved the crash completely.
2. Wiring LSN-Consistent Snapshot Reads Across pg_mooncake and duckdb_mooncake
Because pg_mooncake ingests heap writes asynchronously via PostgreSQL logical replication, a SELECT issued immediately after an INSERT or COMMIT must wait until moonlink has ingested WAL up to the current flush position.
In v0.3.0, we completed the cross-library LSN bridge:
- In
pg_mooncake(src/lib.rs), we exported anextern "C"symbol: - In
duckdb_mooncake, the storage extension resolvesmooncake_get_postgres_lsnat runtime viadlsym(RTLD_DEFAULT, "mooncake_get_postgres_lsn")(falling back to0whenduckdb_mooncakeis loaded inside a standalone DuckDB CLI session outside PostgreSQL). - When binding a scan on a
columnstoretable,duckdb_mooncakepasses that LSN tomoonlink_scan_table_begin(...), which blocks until Moonlink’s WAL consumer reaches the target LSN.
3. Automatic REPLICA IDENTITY FULL on Partitioned Tables
PostgreSQL logical replication requires REPLICA IDENTITY FULL on tables that do not have a primary key (or where Moonlink needs the full before-image of updated/deleted tuples). When a user mirrors a partitioned table via CALL mooncake.create_table('sales_col', 'sales'), setting REPLICA IDENTITY FULL only on the parent partitioned table does not propagate to existing leaf partitions.
pg_mooncake 0.3.0 now uses pg_sys::find_all_inheritors(relid, pg_sys::AccessExclusiveLock as i32, std::ptr::null_mut()) during mooncake.create_table to set REPLICA IDENTITY FULL on the root table and all existing child partitions in a single atomic operation.
4. End-to-End Verification
The adapted stack was validated across two clean Linux ARM64 environments:
- Ubuntu 24.04.5 LTS ARM64 (
mooncake-u24a-pg18, PostgreSQL18.6) - Rocky Linux 10.2 ARM64 (
mooncake-el10a-pg17, PostgreSQL17.11)
All 58 Moonlink table_handler unit tests, all 3 SQL regression suites (small, partition, unsupported), the runtime pg_duckdb 1.2.0 version guard, and 13 post-restart persistence and Iceberg metadata checks passed with zero failures.