DuckLake
The ducklake connector reads and writes a DuckLake lakehouse through DuckDB’s own ducklake
extension. A lake is a metadata catalog plus a data path where DuckLake writes Parquet files. The
engine’s session attaches the lake once per connection, and every read and write is a plain SQL
statement against that attach. The connector runs on the native tier only.
Connection
Section titled “Connection”Every connection names a catalog and, for every catalog except a local duckdb file, a
data_path. Each catalog has its own required keys, and a key that belongs to a different catalog
is refused with an error naming the catalog it belongs to.
catalog |
Required keys | Optional keys | Where the metadata lives |
|---|---|---|---|
duckdb (default) |
path |
data_path |
A DuckDB file. |
sqlite |
path, data_path |
– | A SQLite file. |
postgres |
host, database, data_path |
port (5432), user, password |
A Postgres database. |
quack |
uri, token, data_path |
– | A DuckDB server reached over the Quack protocol. |
motherduck |
database, token, data_path |
– | A MotherDuck-hosted database. |
lake: connector: ducklake path: data/lake.ducklake data_path: data/lake/lake: connector: ducklake catalog: postgres host: pg.internal database: lake_catalog user: pz password: ${LAKE_PG_PASSWORD} data_path: s3://my-bucket/lake/lake: connector: ducklake catalog: motherduck database: my_db token: ${MOTHERDUCK_TOKEN} data_path: s3://my-bucket/lake/The quack catalog accepts quack:host, quack:host:port, or quack://host[:port] as its uri,
and normalizes all three to quack:host:port with a default port of 9494.
Relative path and data_path values resolve against the project directory. There is no
base_dir option. Neither may resolve inside the project’s .pz/ directory. An object-store
data_path is any value containing ://.
Object-store credentials
Section titled “Object-store credentials”When data_path is an object-store URL, the connection may carry S3-compatible credentials. They
build a DuckDB secret scoped to that exact data_path and nothing else.
| Key | Required | Default | Meaning |
|---|---|---|---|
storage_key_id |
With storage_secret_key |
– | Access key id. |
storage_secret_key |
With storage_key_id |
– | Secret key. |
storage_region |
No | us-east-1 |
Region. |
storage_endpoint |
No | – | host:port of an S3-compatible endpoint. |
storage_url_style |
No | vhost |
vhost or path. |
storage_use_ssl |
No | true |
Whether to use TLS. |
The two credential keys are declared together or not at all, and any storage_* key requires
both. Declaring them against a local data_path is refused.
An entity is table (in the lake’s main schema) or schema.table. The connector does not create
schemas.
Read options
Section titled “Read options”| Key | Required | Default | Meaning |
|---|---|---|---|
columns |
No | – | Column name to type map. When declared, the read projects only these columns. |
version |
No | – | Snapshot id to read, for time travel. |
timestamp |
No | – | Read the latest snapshot at or before this instant. Never together with version. |
lake: connector: ducklake path: data/lake.ducklake entities: events: read: columns: { id: bigint, updated_at: timestamp, amount: double } sync: { mode: incremental, cursor: updated_at } events_as_of: read: version: 42A read compiles to a select over the attached table, time-travelled when asked, with the
incremental watermark and any bounded window pushed into the query.
sync and retry under read: are the shared keys documented in the
connections.yml reference.
Write options
Section titled “Write options”ducklake takes no connector-specific write options. It supports every strategy:
| Strategy | What runs |
|---|---|
append |
CREATE TABLE IF NOT EXISTS from the staged rows’ shape, then insert, matched by column name rather than position. |
replace |
One CREATE OR REPLACE TABLE … as SELECT. |
merge |
CREATE TABLE IF NOT EXISTS, then DuckDB’s own merge into, matched on keys:. |
Each generated statement commits as one DuckLake snapshot. There is no transaction spanning several writes.
strategy, keys, schema_policy, and retry under write: are the shared keys documented in
the connections.yml reference.
Capabilities
Section titled “Capabilities”| Flag | Meaning |
|---|---|
NativeScan |
Reads compile to a select over the attached lake. |
NativeCopy |
Writes compile to native insert, create-or-replace, or merge statements. |
ReplaceWrites |
Supports strategy: replace. |
Merge |
Supports strategy: merge through DuckDB’s merge into. |
Transactional |
Each generated statement commits as one snapshot. |
BoundedWindow |
Pushes a watermark upper bound into the generated query. |
InclusiveWatermarkBound |
Accepts an inclusive lower watermark bound (cursor >= value). |
ducklakeis native-only. Declaringengine.force_universalon aducklakeentity fails at plan time; remove that setting instead.appendmatches columns by name, not position. A target column the pipeline does not produce keeps its existing default; a column the pipeline produces that the target lacks is an error naming it.- Credentials never appear in an attach string or an error. Postgres credentials, the quack token, the MotherDuck token, and storage credentials each ride a DuckDB secret or session setting. A failed attach names only a path, a URI, or a database.
- A read of a missing catalog file is refused at plan time for the
duckdbandsqlitecatalogs, for the same reason as the duckdb connector, and with the same deferral for a reader outside the run’s selection. A server catalog has no local file to check. - Use one connection per catalog file. Two connections naming the same
duckdborsqlitecatalog file cannot both attach it in one session. - Duplicate keys within a merge batch collapse to one survivor, as on the
duckdb connector: the generated
MERGE INTOkeys the staged side unique first, and the engine warns withPZ0522. - One MotherDuck token per run. A
motherduckcatalog shares the rule described on the MotherDuck connector page.
pz validate --connect is shallow, because the connector has no driver of its own:
| Catalog | Check |
|---|---|
duckdb |
An existing path must carry the right file header; a zero-byte file is refused, matching what pz run does when it attaches. A missing file is reported as will-be-created; a missing parent directory fails. |
sqlite |
An existing path must carry the right file header, except a zero-byte file, which is accepted — the sqlite extension initializes an empty file as a new catalog on attach, so the probe agrees. A missing file is reported as will-be-created; a missing parent directory fails. |
postgres, quack |
TCP reachability of the host and port, with a five-second timeout. Credentials are verified by the first run. |
motherduck |
Not checked. The first run authenticates. |
| Code | When |
|---|---|
PZ0209 |
A strategy: merge write declares no keys:. |
PZ0311 |
A setup statement failed at run time: a secret, a session setting, or the attach. |
PZ0312 |
engine.force_universal is set on a ducklake entity. |
PZ0353 |
A read the run executes names a catalog file that does not exist yet, or declares both version and timestamp. |
PZ0606 |
Under pz mcp, path or a local data_path resolves outside the project directory. |
Related
Section titled “Related”- DuckDB is the single-file variant with the same SQL surface.
- MotherDuck and Quack describe the two remote catalogs’ own connection rules.
- Incremental loads explains watermarks and bounded windows.
- Secure connection config covers keeping catalog and storage credentials out of the repository.
- Connectors compares every builtin connector’s capabilities at a glance.