Transform Databricks Iceberg via a MotherDuck Staging Table
I keep my lakehouse in Databricks Unity Catalog and want a scheduled Flight that reads an Iceberg table, transforms it into a MotherDuck table I can query and share, and then publishes that table back to Iceberg. Help me adapt the "Transform Databricks Iceberg via a MotherDuck Staging Table" recipe to my own data and use case, using it as a guide: https://motherduck.com/docs/cookbook/flight-iceberg-databricks-stage
A single-file Flight that reads an Iceberg table in Databricks Unity Catalog (UC), transforms and materializes the result in a MotherDuck table, then publishes that table back to Iceberg. You get the curated result in two places: a MotherDuck table you can query, share, and build Dives on, and the open Iceberg lake as the system of record.
This is the staging counterpart to the direct recipe. Use this one when the result should be usable inside MotherDuck (fast reads, sharing, Dives, BI) or when you want to inspect or validate the rollup before it reaches the shared lake; use the direct one when the output only needs to live in Iceberg and you would rather not keep a copy.
The default transform is a daily usage rollup (usage_events_raw to
usage_daily_rollup), so a deploy against that schema produces a successful run
you can then point at your own tables and transform.
How it works
flight.py runs a fixed sequence; config values change its inputs:
- Connect to MotherDuck (
md:),INSTALL/LOAD iceberg, and idempotently attach the Databricks UC Iceberg REST catalog using a MotherDuck secret. - Stage:
CREATE OR REPLACE TABLEthe transform result into a MotherDuck table (reading the source Iceberg table, transforming on MotherDuck's engine). - Publish: replace the Iceberg target rows in one transaction using
DELETEandINSERT ... SELECTfrom the staging table. A failed publish rolls back the Iceberg snapshot. SetPUBLISH_TO_ICEBERG=falseto stage only. - Log staged and published row counts.
The transform is a plain SELECT, so adapt it by replacing the SELECT in the
stage step and the matching CREATE TABLE column list in the publish step.
Questions to answer
- What is your UC Iceberg REST endpoint (
ICEBERG_ENDPOINT) and warehouse (ICEBERG_WAREHOUSE)? - Which secret holds the Databricks token (
ICEBERG_SECRET)? - Which schema and tables (
ICEBERG_SCHEMA,SOURCE_TABLE,TARGET_TABLE)? - Where should the MotherDuck copy live (
MD_DATABASE,MD_SCHEMA,MD_TABLE)? - What is the transform, and does the Iceberg target's explicit column list match it?
- What schedule (cron) should it run on?
Caveats
These are the setup gotchas that make Databricks UC Iceberg writes work from an external engine. They cost hours the first time; get them right before deploying.
- The token needs
all-apisscope. A scope-limited OAuth token 403s at/v1/config. A classic Databricks PAT withall-apisworks. Store it as a MotherDuck Iceberg-catalog secret and pass the secret name usingICEBERG_SECRET. - Writes need credential vending, and the managed catalog does not vend. The
default managed
workspacecatalog will not vend credentials to external engines. You need a UC storage credential + external location (an S3 bucket with a Databricks-provisioned IAM role using the AWS delegation flow),EXTERNAL USE SCHEMA, and external data access enabled. Create native Iceberg tables in a schema on that external location: those vend. Pre-existing Delta-as-Iceberg tables are read-only and will not vend, so you cannot write to them. - The AWS "60-minute temporary access" delegation screen provisions a persistent IAM role. Vending keeps working after the 60 minutes; that screen is a one-time setup step, not a time limit on the integration.
- Pin DuckDB. Server-side Iceberg needs DuckDB >= 1.5.2, and the Flights runtime
otherwise pulls the latest. This template pins
duckdb==1.5.5. - Use distinct source and target names. Matching names, including case-only differences, fail before connecting. The staging database must also differ from the Iceberg catalog.
- The rollup uses UTC days and BIGINT customer IDs.
event_tsmust be castable to a date andcustomer_idto BIGINT. Null keys are retained as groups. - Targets must support merge-on-read deletes. Use an unsorted native Iceberg table with a compatible schema. Unsupported delete modes fail the transaction without dropping the previous result. Existing schemas are not migrated.
- An existing catalog keeps its saved connection settings.
ICEBERG_ENDPOINT, warehouse, secret, and default schema configure initial creation. Use a new catalog name or explicitly update its settings when changing the connection. Reusing a native MotherDuck database name is rejected. - The staged MotherDuck table is fully replaced each run (
CREATE OR REPLACE). Iceberg rows are refreshed in a separate transaction. A failed publish preserves the old Iceberg output but leaves the new MotherDuck staging table available. The two destinations do not commit atomically together. - Reads only? Staging still needs vending. Reading data files from UC also needs
vending, so the same external-location setup applies even if you set
PUBLISH_TO_ICEBERG=false. - Keep the token out of config. The Databricks credential lives in the secret;
a MotherDuck token is injected as
MOTHERDUCK_TOKENat runtime. Never put either inconfig.
What you'll adjust
| Config key | Default | Purpose |
|---|---|---|
ICEBERG_ENDPOINT | (required) | UC Iceberg REST endpoint, e.g. https://<host>/api/2.1/unity-catalog/iceberg-rest. |
ICEBERG_WAREHOUSE | workspace | UC catalog/warehouse name. |
ICEBERG_SECRET | databricks_token | Name of the MotherDuck secret holding the all-apis token. Validated as an identifier. |
ICEBERG_CATALOG | databricks_iceberg | Local attach name for the catalog. Validated as an identifier. |
ICEBERG_DEFAULT_SCHEMA | default | default_schema for the attach. |
ICEBERG_SCHEMA | md_iceberg_demo | Working schema in the catalog. Validated as an identifier. |
SOURCE_TABLE | usage_events_raw | Source Iceberg table. Validated as an identifier. |
TARGET_TABLE | usage_daily_rollup | Target Iceberg table (rows refreshed when publishing). Validated as an identifier. |
MD_DATABASE | flights_demo | MotherDuck database for the staged table. Created if missing. Validated as an identifier. |
MD_SCHEMA | main | MotherDuck schema for the staged table. Validated as an identifier. |
MD_TABLE | usage_daily_rollup | MotherDuck staging table name. Validated as an identifier. |
PUBLISH_TO_ICEBERG | true | Set false to stage only. Only true and false are accepted. |
MOTHERDUCK_TOKEN | (Flight-injected) | Auth. Select a token on the Flight; never put it in config. |
Run it
You need a MotherDuck account and token, a Databricks UC catalog reachable over the
Iceberg REST endpoint, and a MotherDuck Iceberg-catalog secret with an all-apis
token. To smoke-test before deploying:
export MOTHERDUCK_TOKEN=your_token_here
ICEBERG_ENDPOINT='https://<host>/api/2.1/unity-catalog/iceberg-rest' \
uv run --with duckdb==1.5.5 flight.py
Include PUBLISH_TO_ICEBERG in the Flight config at creation if you want to
override it per run. Run overrides can only change keys already defined in config.
Set PUBLISH_TO_ICEBERG=false on the first run to build the MotherDuck staging
table and inspect it before writing anything to Iceberg.
Deploy as a Flight
Create the Flight with MD_CREATE_FLIGHT, passing source_code from
flight.py,
requirements_txt from requirements.txt,
and the config keys you want to override. A MotherDuck token is attached
automatically and injected as MOTHERDUCK_TOKEN; no token argument is needed. Run
once manually with MD_RUN_FLIGHT, confirm it succeeds, then add a schedule with
MD_UPDATE_FLIGHT (schedule updates are metadata-only).
Security
- Identifier validation. The catalog, secret name, schemas, and table names flow
into
ATTACH/CREATE/INSERTstatements that cannot be parameterized, so each is checked against^[A-Za-z_][A-Za-z0-9_]*$and double-quoted before SQL runs. The endpoint, warehouse, and default schema are inlined as escaped string literals. - Credential in a secret. The Databricks token never appears in code or config; it is referenced by secret name and read by the engine at attach time.
Validation
The local development test uses a real Iceberg REST catalog and S3-compatible storage, with MotherDuck compute and on-demand managed Flights. It covers exact rollup rows, repeat runs, large IDs, nulls, UTC boundaries, staging-only mode, and rollback after a failed publish. See the integration test guide. Databricks authentication and credential vending require a separate UC workspace and are not validated by the local catalog test.
Learn more
- MotherDuck Iceberg integration.
- DuckDB Iceberg writes and limitations.
- Flight mechanics (creating, running, scheduling): use the MotherDuck MCP
get_flight_guidetool. - Deeper MotherDuck or DuckDB questions (server-side Iceberg, UC credential vending):
use the
ask_docs_questionMCP tool. - Files in this template:
flight.pyandrequirements.txt.