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: