DataFusion-DuckLake
A DataFusion extension for reading and writing DuckLake catalogs. DuckLake is an integrated data lake and catalog format that stores metadata in a SQL database and data as Parquet files on disk or object storage.
The goal of this project is to make DuckLake a first-class, Arrow-native lakehouse format inside DataFusion.
This project is maintained by Hotdata with support from the community. Come talk to us on the Hotdata Discord.
- 📦 crates.io: https://crates.io/crates/datafusion-ducklake
- 📖 API docs: https://docs.rs/datafusion-ducklake
- 🧩 Feature & backend support: see COMPATIBILITY.md
- 💬 Project chat: DataFusion+DuckLake Discord — development and usage discussion
- 🧡 Meet the team: Hotdata Discord
Quick start
Add the crate:
The default build includes the SQLite catalog backend. Every other catalog backend, and write support for all of them, is opt-in via feature flags; applications configure their object store implementation directly. See COMPATIBILITY.md for the full matrix.
The DuckDB backend statically compiles DuckDB, which is by far the slowest dependency in this crate to build, so it is not a default:
# Cargo.toml — read (and write) DuckDB-backed catalogs
[]
= "0.9"
= ["duckdb-bundled"] # add "write-duckdb" to write them
# Cargo.toml — read PostgreSQL catalogs
# (to write them too, use features = ["write-postgres"])
[]
= "0.9"
= false
= ["metadata-postgres", "tls-rustls-aws-lc-rs"]
metadata-postgres, multicatalog-postgres, and write-postgres do not select a TLS provider.
Plain local connections work without one. For TLS, also enable one of tls-native-tls,
tls-rustls-aws-lc-rs, or tls-rustls-ring on datafusion-ducklake.
The examples below also use datafusion, object_store, and url directly — add them
to your [dependencies] as well (this crate does not re-export them). The write example
additionally uses sqlx (with its postgres and runtime-tokio features) to open the
connection pool.
Run a query against an existing PostgreSQL catalog with the bundled example:
Configure object_store directly for local, S3, or MinIO data files. Applications can instead
register another DataFusion ObjectStore, such as an OpenDAL‑backed connector.
DataFusion enables local filesystem support. S3 and MinIO applications must enable
object_store/aws themselves. That feature enables Ring through its HTTP client. If another
dependency enables AWS‑LC, install the intended process‑wide Rustls
CryptoProvider before
creating TLS clients.
(The example also accepts DuckDB, SQLite, and MySQL connection strings with the matching
metadata-* feature — see COMPATIBILITY.md.)
Reading a catalog
Register a DuckLakeCatalog with a SessionContext and query it with normal SQL as
catalog.schema.table:
use RuntimeEnv;
use *;
use ;
use ObjectStore;
use AmazonS3Builder;
use Arc;
use Url;
// (inside an async fn)
// Read metadata from a PostgreSQL catalog
let provider = new.await?;
// Register object stores for any non-local data (S3 / MinIO)
let runtime = new;
let s3: = new;
runtime.register_object_store;
let catalog = new?;
let ctx = new_with_config_rt;
register;
let df = ctx.sql.await?;
df.show.await?;
Writing a catalog
Data inlining is opt-in. The default data_inlining_row_limit = 0 keeps writes
in Parquet. A positive limit enables eligible small writes; see the
inlined storage limits before enabling it.
PostgreSQL has two writers, both behind the write-postgres feature:
PostgresSingleCatalogMetadataWriter— the standard, spec-compliant single-catalog layout. Same catalog shape as the SQLite and MySQL writers, so the catalog is readable (and writable) by other DuckLake implementations including DuckDB'sducklakeextension. SQLCREATE TABLEandINSERT INTOboth work. Prefer this one.PostgresMetadataWriter— the experimental multi-catalog layout described in its own section, for hosting many catalogs in one database. Library-specific, not in the DuckLake spec, and no CTAS.
use *;
use MetadataWriter; // set_data_path
use ;
use Arc;
// Bootstrap the standard DuckLake tables and point the catalog at its data root
let writer = new_with_init.await?;
writer.set_data_path?;
// SQL planning requires the target schema to exist
let snapshot = writer.create_snapshot?;
writer.get_or_create_schema?;
let provider = new.await?;
let catalog = with_writer?;
let ctx = new;
register;
ctx.sql.await?.collect.await?;
Non-empty CREATE TABLE AS SELECT is rejected before publishing metadata.
After creating an empty table, reopen the catalog before using
INSERT INTO ... SELECT to populate it.
The multi-catalog path instead looks like this — tables are created through the writer API (no CTAS), then appended to with SQL:
use *;
use MetadataWriter; // set_data_path
use ;
use PgPoolOptions;
use Arc;
let pool = new.connect.await?;
// One-time: bootstrap the multi-catalog tables, then create a named catalog
initialize_multicatalog_schema.await?;
let catalog_id = new.create_catalog.await?;
// Create a table by writing the first batch through the table writer
let writer = new;
writer.set_data_path?;
let object_store: =
new;
let table_writer = new?;
table_writer.write_table.await?; // `batch` is your RecordBatch
// Now append with SQL, reading the same catalog back through MulticatalogProvider
let provider = with_pool.await?;
let catalog = with_writer?;
let ctx = new;
register;
ctx.sql.await?.collect.await?;
ctx.sql.await?.show.await?;
Writer output is configurable (Parquet compression, row-group sizing by row count and
byte size). See DuckLakeTableWriter for the
writer options.
Writing to a standard, single-catalog DuckLake store is supported through
DuckdbMetadataWriter,SqliteMetadataWriter,MySqlMetadataWriter, andPostgresSingleCatalogMetadataWriterwith their matchingwrite-*features. Seetests/it/sql_write_tests.rsandtests/it/postgres_single_catalog_write_tests.rs.
Partitioning
Partition a table by one or more columns (optionally through a transform) so that queries filtering on a partition column skip whole files:
// Declare the partition scheme *before* loading data, then INSERT as usual.
execute_ducklake_sql
.await?;
Writes split rows into one Parquet file per partition value; reads then prune non-matching
files automatically. Supported transforms are identity (the raw value) and
year/month/day/hour; pruning currently applies to identity and year
(month/day/hour are recorded but not yet used to skip files). Partitioned writes
work on every writable backend — SQL INSERT/UPDATE, the low-level write entry points,
the streaming session, compaction, and promote all honour the live spec — and read +
pruning work on all backends. RESET PARTITIONED BY turns it off. See
COMPATIBILITY.md and
tests/it/partition_write_tests.rs.
Sort order
Order the rows inside each written file so that per-file statistics stay tight and range-filtered scans skip more:
execute_ducklake_sql
.await?;
The spec is recorded in the catalog (ducklake_sort_info / ducklake_sort_expression) and
applied on insert, to UPDATE rewrites, and to compaction output; rows are sorted before the
partition split, so each per-partition file stays a sorted subsequence. Bare-column keys are
produced; other expressions are tolerated on read but never produced. RESET SORTED BY
turns it off.
Multi-catalog (PostgreSQL, experimental)
A single PostgreSQL metadata store can host multiple independent DuckLake catalogs — useful for multi-tenant deployments or keeping many logical lakehouses in one database.
⚠️ Experimental and library-specific. This multi-catalog layout is not part of the DuckLake specification and is not (yet) supported or accepted upstream. Catalogs written this way are read back only through this crate's
MulticatalogProvider— they are not interchangeable with a standard single-catalog DuckLake store. The API and on-disk/in-catalog layout may change, so treat it as a preview. PostgreSQL writes no longer require this path — usePostgresSingleCatalogMetadataWriterfor the spec-compliant layout.
- Create and manage catalogs with
MulticatalogManager(featurewrite-postgres):initialize_multicatalog_schemabootstraps the shared tables, thencreate_catalog, anddrop_table_in_catalogmanage their contents. - Read a specific catalog with
MulticatalogProvider::with_pool(pool, "name")(featuremulticatalog-postgres), which plugs into aDuckLakeCataloglike any other metadata provider.
See examples/multicatalog_write.rs for an end-to-end
walkthrough (bootstrap → create catalogs → write → read back).
Maintenance
The maintenance API handles lakehouse upkeep from Rust: expiring old snapshots,
cleaning up superseded files, and reclaiming orphaned files. The concrete entry points
are backend-gated (write-sqlite / write-postgres). DROP TABLE is available through
MetadataWriter. See
examples/maintenance_demo.rs and
examples/orphan_cleanup_demo.rs.
Compaction
Two explicit, triggered operations on DuckLakeTable rewrite a table's data files
into a better physical layout without changing its logical rows:
merge_adjacent_files(state, MergeOptions)coalesces several small files (of the same schema version) into fewer larger ones. A merged file spanning multiple origin snapshots is written as a DuckLake partial data file (preserving each row's original rowid and origin snapshot), so time travel and change feeds are unaffected.rewrite_data_files(state, RewriteOptions)rewrites a file whose deleted fraction exceeds a threshold (default0.95), dropping its deleted rows.
Both commit atomically in one snapshot and coexist with concurrent appends; superseded
files are scheduled for deletion and reclaimed later by cleanup_old_files. See
examples/compaction_demo.rs.
Compatibility
For the full breakdown of catalog backends, object stores, types, capabilities, and current limitations, see COMPATIBILITY.md.
A few highlights worth knowing up front:
- Reads and writes work on DuckDB, SQLite, PostgreSQL, and MySQL. PostgreSQL supports the standard single-catalog layout, recommended by default, and an experimental multi-catalog layout.
- Object stores: local filesystem, S3-compatible (S3, MinIO), and any other
object_storebackend registered in theRuntimeEnvunder thedata_pathscheme (gs://,az://, ...). - A catalog from
DuckLakeCatalog::neworwith_writerreads the latest snapshot at each lookup. Register it withDuckLakeCatalog::registerto keep each statement on one snapshot. - Snapshots can be selected through
DuckLakeCatalog(by id or timestamp) or per query withducklake_table_at; DataFusion does not supportAS OFsyntax. - Comments and tags are readable on every backend through the
information_schemaviewsobject_tagsandcolumn_tags. SQLite and both PostgreSQL layouts can write them, for example withCOMMENT ONthroughexecute_ducklake_sql. - Table partitioning: read + file pruning on all backends; partitioned writes on every writable backend.
- Filter pushdown reaches the catalog: per-column statistics narrow the file-listing query itself, so a selective scan no longer lists every file. A few type/backend combinations decline it and fall back to in-memory pruning — see COMPATIBILITY.md.
- Data inlining: rows inlined by DuckDB's ducklake extension are read and counted by
COUNT(*)on every backend. Writing small batches inline is opt-in via thedata_inlining_row_limitsetting;UPDATEand row-lineage scans still refuse a table with visible inlined rows — see COMPATIBILITY.md. - Schema evolution: recursive
list/struct/mapcolumns, literal column defaults, and per-filemap_by_namename mappings are honoured across scans, writes, and change feeds. - Read-only DuckLake views are exposed across metadata backends.
ORDER BY col LIMIT nprunes data files whose statistics cannot beat the running Top-N boundary, and an unfilteredCOUNT(*)/MIN/MAXfolds to a literal from catalog statistics.
Running the tests
The test fixtures are built by driving the real DuckLake extension through a bundled DuckDB, and
those modules are gated on metadata-duckdb. Since that is not a default feature, a bare
cargo test compiles only a small fraction of the suite:
The PostgreSQL, MySQL, and multi-catalog suites need Docker (via testcontainers). Note that
--all-features enables skip-tests-with-docker, which ignores those suites rather than
running them, so a green --all-features run is not evidence that they passed.
Project status
This project is in alpha and evolving alongside DataFusion and DuckLake. APIs may change as core abstractions are refined. See CHANGELOG.md for release history. Feedback, issues, and contributions are welcome.