Apache Iceberg
Attach an Iceberg REST catalog as a MotherDuck database to read and write Iceberg tables, or scan individual tables by path.
MotherDuck supports the Apache Iceberg format through the DuckDB Iceberg extension.
There are two ways to work with Iceberg in MotherDuck:
- Persisted Iceberg catalogs — attach an Iceberg REST catalog as a MotherDuck database with
CREATE DATABASE. The attachment lives in your workspace, so it survives across sessions, and reads and writes run on MotherDuck's compute. - Scanning individual tables — query a single Iceberg table by path with
iceberg_scan, without attaching a catalog.
Persisted Iceberg catalogs
Attach an Iceberg REST catalog as a MotherDuck database. The database persists in your workspace: you attach it once, it appears alongside your other databases, and you don't re-attach it in each new session. Reads and writes run on MotherDuck's cloud execution engine.
Persisted Iceberg catalogs require DuckDB 1.5.2 or later. The catalog is attached, browsed, and queried on MotherDuck's compute, so your client doesn't need the DuckDB Iceberg extension.
MotherDuck works with any Iceberg REST catalog endpoint.
| Catalog | Read | Write |
|---|---|---|
| Amazon S3 Tables | ✅ Yes | ✅ Yes |
| Apache Polaris | ✅ Yes | ✅ Yes |
| AWS Glue | ✅ Yes | ✅ Yes |
| Cloudflare R2 | ✅ Yes | ✅ Yes |
| Databricks Unity Catalog | ✅ Yes | ✅ Yes (tables must be backed by external storage) |
- Write operations use Iceberg's merge-on-read model and are subject to the Limitations below.
- AWS Glue:
CREATE TABLErequires an explicitlocation. See AWS Glue for details.- AWS Lake Formation: Access is validated for reads. Grants must cover whole tables, so writes through Lake Formation credential vending are not validated. See Lake Formation permissions.
- Databricks Unity Catalog: Only for tables stored on external locations. Writes apply to Unity Catalog-managed Iceberg tables. Delta tables exposed through the Iceberg REST endpoint are read-only. See Databricks for details.
- Cloudflare R2 Data Catalog: Table data lives in R2 object storage (S3-compatible). Authenticate with a Cloudflare API token that has R2 Data Catalog permission. See Cloudflare R2 for details.
Iceberg REST catalog reads and writes run on MotherDuck's cloud compute. Attaching an Iceberg REST catalog directly in a local DuckDB session without MotherDuck is not recommended. Attach the catalog as a MotherDuck database instead.
Authentication
Store your catalog credentials in a MotherDuck secret. Credentials must live in a secret — the database options accept catalog settings only, not credentials.
-- OAuth2 client credentials
CREATE SECRET my_iceberg_secret IN MOTHERDUCK (
TYPE ICEBERG,
CLIENT_ID 'my_client_id',
CLIENT_SECRET 'my_client_secret',
OAUTH2_SERVER_URI 'https://my-catalog.example.com/v1/oauth/tokens'
);
-- Bearer token
CREATE SECRET my_iceberg_secret IN MOTHERDUCK (
TYPE ICEBERG,
TOKEN 'my_bearer_token'
);
See CREATE SECRET for the full list of Iceberg secret parameters.
Creating the database
CREATE DATABASE ... TYPE ICEBERG does not create a new Iceberg catalog. It connects to an existing REST catalog and registers it as a MotherDuck database, behaving like an attach. The catalog must already exist at the endpoint you point to.
Create the database with TYPE ICEBERG, referencing the secret and the catalog endpoint. A default_schema that exists in the catalog is required:
CREATE DATABASE my_datalake (
TYPE ICEBERG,
"secret" my_iceberg_secret,
endpoint 'https://my-catalog.example.com',
warehouse 'my_warehouse',
default_schema 'default'
);
Once attached, browse and query the catalog with standard SQL:
-- List schemas
SELECT schema_name FROM information_schema.schemata
WHERE catalog_name = 'my_datalake';
-- List tables in a schema
SHOW TABLES FROM my_datalake.my_schema;
SHOW SCHEMAS IN my_datalake;
-- Inspect a table's columns (duckdb_columns() does not list them)
DESCRIBE my_datalake.default.my_table;
-- Query a table
SELECT * FROM my_datalake.default.my_table;
Set the database as the active catalog to use unqualified names:
USE my_datalake;
SELECT * FROM my_table;
Database options
Pass these options in the CREATE DATABASE options list. Credentials (CLIENT_ID, CLIENT_SECRET, OAUTH2_*, TOKEN) belong in the secret, not here.
| Option | Description |
|---|---|
secret | Required. Name of the MotherDuck Iceberg or S3 secret holding catalog credentials. Quote as "secret". |
endpoint | URL of the Iceberg REST catalog. Required unless the endpoint is set in the secret or derived from endpoint_type. |
warehouse | Catalog warehouse identifier. For S3 Tables, this is the bucket ARN. For Cloudflare R2, this is <account_id>_<bucket_name>, a mandatory input. |
default_schema | Required. Schema used to resolve unqualified table names. Must exist in the catalog. |
endpoint_type | Selects a well-known catalog flavor, for example 's3_tables' or 'glue'. |
default_region | Per-catalog region override. Defaults to your MotherDuck org region. |
read_only | Attach the catalog as read-only. |
access_delegation_mode | Whether to request vended credentials from the catalog. 'vended_credentials' (default) requests short-lived, table-scoped credentials when the catalog supports them; 'none' uses the secret's credentials directly. |
For the full set of catalog options, see the DuckDB Iceberg REST catalog documentation.
Changing database options
Use ALTER DATABASE to update an attached catalog's configuration. MotherDuck reattaches the catalog right away, so the next query uses the new settings:
-- Resolve unqualified table names against a different namespace
ALTER DATABASE my_datalake SET default_schema = 'analytics';
-- Point the database at a rotated secret
ALTER DATABASE my_datalake SET secret = 'my_new_iceberg_secret';
secret, default_schema, default_region, access_delegation_mode, and the catalog behavior toggles can be altered; secret and default_schema can't be cleared once set.
The options that identify the catalog itself - endpoint, warehouse, endpoint_type, and read_only - can't be altered, because changing them points the database at a different catalog: that's a different database, not a reconfigured one. To change one of those, drop the database and create it again. Refer to ALTER DATABASE for the full list.
Amazon S3 Tables
For Amazon S3 Tables, authenticate with an S3 secret (SigV4) and set endpoint_type to 's3_tables'. The warehouse is the table bucket ARN, and the endpoint is derived from it.
CREATE SECRET s3_tables_secret IN MOTHERDUCK (
TYPE S3,
KEY_ID '<aws_access_key_id>',
SECRET '<aws_secret_access_key>',
REGION 'us-east-1'
);
CREATE DATABASE my_s3_tables (
TYPE ICEBERG,
endpoint_type 's3_tables',
warehouse 'arn:aws:s3tables:us-east-1:<account_id>:bucket/<bucket_name>',
"secret" s3_tables_secret,
default_schema 'default'
);
AWS Glue
For the AWS Glue Data Catalog, authenticate with an S3 secret (SigV4) and set endpoint_type to 'glue'. The warehouse is your AWS account ID, and the endpoint is derived from the secret's REGION. Each Glue database becomes a schema; pass one that exists as default_schema.
CREATE SECRET glue_secret IN MOTHERDUCK (
TYPE S3,
KEY_ID '<aws_access_key_id>',
SECRET '<aws_secret_access_key>',
REGION '<aws_region>'
);
CREATE DATABASE my_glue_catalog (
TYPE ICEBERG,
endpoint_type 'glue',
warehouse '<aws_account_id>',
"secret" glue_secret,
default_schema '<glue_database_name>'
);
If your lake doesn't use AWS Lake Formation, this is the complete setup: the IAM principal in the secret needs the Glue catalog read actions (glue:GetCatalog, glue:GetDatabase, glue:GetDatabases, glue:GetTable, glue:GetTables) plus s3:GetObject on the table locations, and kms:Decrypt if the bucket uses SSE-KMS. The rest of this section covers Lake Formation–governed lakes.
Lake Formation prerequisites
When your S3 locations are registered with AWS Lake Formation, data access is handled by Lake Formation's credential vending: at query time, AWS issues short-lived, table-scoped S3 credentials to MotherDuck as an external engine. MotherDuck does not vend credentials itself; it presents the secret's IAM principal, and Lake Formation decides what it can read. For tables in registered locations, that principal needs no S3 permissions: access is granted per table by your existing Lake Formation permissions, including tag-based access control.
Four one-time settings enable credential vending for external engines:
-
Allow full table access for external engines. In the Lake Formation console under Administration → Application integration settings, enable Allow external engines to access data in Amazon S3 locations with full table access. From the CLI,
put-data-lake-settingsreplaces the entire settings object, so retrieve the current settings first:aws lakeformation get-data-lake-settings --query DataLakeSettings > settings.json# add "AllowFullTableExternalDataAccess": true to settings.jsonaws lakeformation put-data-lake-settings --data-lake-settings file://settings.json -
Register the S3 location with a custom IAM role. Credential vending doesn't work for locations registered with the service-linked role. Register (or re-register) the location with a role that Lake Formation can assume and that has S3 access to the bucket, plus
kms:Decryptif the bucket uses SSE-KMS:aws lakeformation register-resource \--resource-arn arn:aws:s3:::<bucket_name> \--role-arn arn:aws:iam::<aws_account_id>:role/<registration_role> -
Create the IAM principal for MotherDuck. Its policy contains only the Glue catalog read actions listed above and
lakeformation:GetDataAccess. Leave S3 permissions out — Lake Formation vends data access per query:{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": ["glue:GetCatalog","glue:GetDatabase","glue:GetDatabases","glue:GetTable","glue:GetTables"],"Resource": "*"},{"Effect": "Allow","Action": ["lakeformation:GetDataAccess"],"Resource": "*"}]} -
Remove the
IAMAllowedPrincipalsdefaults. By default, Lake Formation grantsIAMAllowedPrincipals— every IAM principal in the account — on new databases and tables. That default satisfies Lake Formation for any principal that can request vended credentials, bypassing your per-principal grants. In the console under Data Catalog settings, clear the Use only IAM access control defaults for new databases and tables, and revoke existingIAMAllowedPrincipalsgrants on the databases and tables you serve.
Lake Formation permissions
In Lake Formation, grant the principal DESCRIBE on the Glue database and SELECT on the tables it should read, either directly or through LF-tags. These grants are managed entirely on the AWS side; MotherDuck presents the principal's identity and Lake Formation decides what it can access. Read-only grants are sufficient. With the IAMAllowedPrincipals defaults removed (prerequisite 4), tables the principal isn't granted don't appear in the attached catalog.
Lake Formation grants must apply to whole tables: Lake Formation can't vend credentials for grants that carry column-level permissions or row and cell filters, so queries against tables with such grants fail instead of returning unfiltered data. This AWS constraint applies to all external engines. To serve filtered data, grant access to a pre-filtered table or view instead.
Troubleshooting Lake Formation errors
Error reference: Lake Formation and Glue REST errors
| Error message | Cause | Fix |
|---|---|---|
Insufficient Lake Formation permissions. Verify the data lake settings for account | Application integration isn't enabled | Enable AllowFullTableExternalDataAccess (prerequisite 1) |
Access is not allowed. | The S3 location is registered with the service-linked role, which doesn't support credential vending | Re-register the location with a custom role (prerequisite 2) |
FULL SELECT or SUPER privileges required on the table. | The principal's SELECT grant is missing, limited to specific columns, or has a row filter | Grant SELECT on the whole table with no filters (see Lake Formation permissions) |
Insufficient Lake Formation permission(s): Required Describe on <table> | The principal has no Lake Formation grant on that table | Grant DESCRIBE and SELECT if the principal should have access |
not authorized to perform: s3:GetObject | The location isn't registered with Lake Formation, so no credentials are vended | Register the location (prerequisite 2), or for lakes without Lake Formation, grant the principal s3:GetObject |
Newly created IAM users, roles, and access keys can take a minute to propagate. If you get a 403 Forbidden right after creating one, retry before changing any settings.
Databricks
Databricks Unity Catalog provides an Iceberg REST catalog endpoint at https://<workspace-url>/api/2.1/unity-catalog/iceberg-rest. You can use that endpoint to attach Unity Catalog as an Iceberg catalog in MotherDuck. This can include Unity Catalog Iceberg tables and Delta tables that are configured for Iceberg reads. Databricks only supports credential vending for tables stored on external locations.
CREATE SECRET databricks_uc_secret IN MOTHERDUCK (
TYPE ICEBERG,
TOKEN '<databricks_personal_access_token>'
);
CREATE DATABASE databricks_uc (
TYPE ICEBERG,
"secret" databricks_uc_secret,
endpoint 'https://<workspace-url>/api/2.1/unity-catalog/iceberg-rest',
warehouse '<uc_catalog_name>',
default_schema '<uc_schema_name>',
read_only false
);
Delta tables exposed through Unity Catalog's Iceberg REST catalog have two limitations:
- They are read-only.
- Iceberg reads need to be enabled, which means using
IcebergCompatV2and disabling deletion vectors.
CREATE OR REPLACE TABLE <table_name> (<table_schema>) TBLPROPERTIES
(
'delta.columnMapping.mode' = 'name',
'delta.enableDeletionVectors' = 'false',
'delta.enableIcebergCompatV2' = 'true',
'delta.universalFormat.enabledFormats' = 'iceberg'
);
For a complete overview of setup requirements see the Databricks Iceberg client access documentation.
Troubleshooting Databricks Iceberg reads
Error reference: Databricks Unity Catalog Iceberg reads
| Error message | Cause | Fix |
|---|---|---|
HTTP 404 / NoSuchKey naming a specific .parquet file | UniForm Iceberg metadata fell behind the Delta log. Attach and listing can still succeed, and a Delta client may still read the table, because those paths use catalog metadata or the Delta log — MotherDuck reads the UniForm Iceberg snapshot through the Iceberg REST endpoint. OPTIMIZE or VACUUM can delete data files that a stale snapshot still references. | In Databricks, run MSCK REPAIR TABLE <catalog>.<schema>.<table> SYNC METADATA and retry the query. Re-attaching the catalog in MotherDuck does not fix this — Databricks must regenerate the Iceberg metadata. |
Permission error: Missing or invalid credentials | The table is on Unity Catalog managed storage (Databricks vends storage credentials only for external locations), or the secret is wrong. The error points at your token even when the cause is the storage location. | Move the table to an external location. Check the table's storage location before rotating your token. |
Cloudflare R2 Data Catalog
Cloudflare R2 Data Catalog exposes an Iceberg REST catalog on top of an R2 bucket. The table data is stored in R2 object storage, which is S3-compatible, and reads and writes run on MotherDuck's compute.
Authenticate with a Cloudflare API token that has R2 Data Catalog permission (for example an Admin Read & Write R2 API token), stored in a TYPE ICEBERG secret as a bearer TOKEN. An R2 object-only token, or an S3 access key and secret, is not sufficient: the catalog rejects it with 401 Unauthorized (wrong token type) or 403 Forbidden (missing Data Catalog permission).
R2 supports credential vending, so with the default access_delegation_mode the same catalog token also authorizes reading and writing the underlying data files. You do not need a separate S3 secret.
CREATE SECRET r2_iceberg IN MOTHERDUCK (
TYPE ICEBERG,
TOKEN '<cloudflare_r2_api_token>'
);
CREATE DATABASE my_r2_catalog (
TYPE ICEBERG,
"secret" r2_iceberg,
endpoint 'https://catalog.cloudflarestorage.com/<account_id>/<bucket_name>',
warehouse '<account_id>_<bucket_name>',
default_schema '<namespace>'
);
The endpoint and warehouse are shown in your bucket's R2 Data Catalog settings. The warehouse (<account_id>_<bucket_name>) field is required; without it, the attach cannot address the catalog.
Enable the catalog on the bucket (npx wrangler r2 bucket catalog enable <bucket_name>) and make sure it contains at least one namespace before attaching. default_schema must reference a namespace that already exists, and a brand-new R2 catalog is empty. Create the first namespace with PyIceberg or the catalog REST API before running CREATE DATABASE. Once attached, you can create tables within existing namespaces from MotherDuck.
Reading and writing
A persisted Iceberg catalog supports standard DDL and DML, executed on MotherDuck's compute: creating schemas and tables, inserting data, partitioned writes, MERGE INTO, and ALTER TABLE.
CREATE SCHEMA my_datalake.analytics;
CREATE TABLE my_datalake.analytics.events (
event_id INTEGER,
event_type VARCHAR,
created_at TIMESTAMP
);
INSERT INTO my_datalake.analytics.events
VALUES (1, 'page_view', '2025-01-15 10:30:00');
ALTER TABLE my_datalake.analytics.events
SET PARTITIONED BY (year(created_at));
On AWS Glue, CREATE TABLE requires an explicit location, because Glue doesn't assign table locations:
CREATE TABLE my_glue_catalog.<glue_database_name>.<table_name> (
id BIGINT
) WITH (
'location' = 's3://<bucket_name>/<path>/<table_name>'
);
Refer to the DuckDB Iceberg documentation for the current support matrix for write operations and time travel.
Never modify Parquet data files or Iceberg metadata files by hand after they've been written. Iceberg treats these files as immutable, and MotherDuck relies on that: snapshots, manifests, and statistics all assume the underlying files never change. Editing, overwriting, or replacing a file in place breaks that assumption and leads to data corruption and incorrect query results.
Writing to the same table from multiple Iceberg writers is supported - the catalog coordinates those writes into new immutable files and snapshots. What's unsafe is mutating a file that has already been written.
Time travel
Query a historical snapshot of a catalog table with the AT clause, by snapshot ID or timestamp:
-- Query a specific snapshot by ID
SELECT * FROM my_datalake.default.my_table
AT (VERSION => 1234567890);
-- Query as of a timestamp
SELECT * FROM my_datalake.default.my_table
AT (TIMESTAMP => TIMESTAMP '2025-01-15 10:30:00');
Limitations
UPDATE,DELETE, andMERGE INTOuse merge-on-read semantics and write positional delete files; copy-on-write is not supported. If a table setswrite.update.modeorwrite.delete.modeto anything other thanmerge-on-read, the operation fails- Iceberg catalogs can't be shared. To give another account access to the same catalog, create the same Iceberg database in that account.
ALTER DATABASEcan't change the options that identify the catalog (endpoint,warehouse,endpoint_type, andread_only). To change one of those, drop and recreate the database. See Changing database options.INSERTandUPDATEare not supported on tables that have a sort order.- Table columns are not populated in
duckdb_columns(). RunDESCRIBE <table>to see a table's columns. - Reading from REST catalogs is limited to S3, S3-compatible object storage (including Cloudflare R2), S3 Tables, and GCS storage backends.
- Converting an Iceberg catalog to DuckLake with
iceberg_to_ducklakeis not supported.
For more details, see the DuckDB Iceberg REST catalog documentation.
Scanning individual Iceberg tables
Unlike a persisted catalog, iceberg_scan and COPY ... TO ... (FORMAT iceberg) are bound by your DuckDB client, so the client needs the Iceberg extension. DuckDB autoloads it on first use. If your environment can't reach extensions.duckdb.org, or you've turned off autoload_known_extensions, install and load it once per environment:
INSTALL iceberg;
LOAD iceberg;
Use iceberg_scan to query individual Iceberg tables directly by path, without attaching a catalog:
SELECT count(*)
FROM iceberg_scan('s3://my-bucket/my-iceberg-table',
allow_moved_paths = true);
To query data in a secure Amazon S3 bucket, you will need to configure your Amazon S3 credentials. If credentials are missing, expired, or lack permission, iceberg_scan fails with No version was provided and no version-hint could be found — check your S3 secret before anything else. Enabling unsafe_enable_version_guessing does not fix a credentials problem.
The allow_moved_paths option is only needed for tables whose files were copied or moved to a different location after they were written (metadata then contains absolute paths that no longer match). Freshly written tables read fine without it.
iceberg_scan parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
allow_moved_paths | BOOLEAN | false | Allow scanning Iceberg tables that have been moved or relocated |
metadata_compression_codec | VARCHAR | '' | Set to 'gzip' to read gzip-compressed metadata files |
snapshot_from_id | UBIGINT | NULL | Query a specific snapshot by ID |
snapshot_from_timestamp | TIMESTAMP | NULL | Query the latest snapshot as of a given timestamp |
version | VARCHAR | '?' | Explicit version string, hint file path, or '?' for auto-detection |
version_name_format | VARCHAR | 'v%s%s.metadata.json,%s%s.metadata.json' | Custom metadata filename pattern |
Time travel with iceberg_scan
-- Query a specific snapshot
SELECT *
FROM iceberg_scan('s3://my-bucket/my-iceberg-table',
allow_moved_paths = true,
snapshot_from_id = 1234567890);
-- Query as of a timestamp
SELECT *
FROM iceberg_scan('s3://my-bucket/my-iceberg-table',
allow_moved_paths = true,
snapshot_from_timestamp = TIMESTAMP '2025-01-15 10:30:00');
Metadata and snapshot functions
Use iceberg_metadata to inspect manifest entries (file paths, formats, record counts):
SELECT *
FROM iceberg_metadata('s3://my-bucket/my-iceberg-table',
allow_moved_paths = true);
Use iceberg_snapshots to list available snapshots:
SELECT *
FROM iceberg_snapshots('s3://my-bucket/my-iceberg-table');
Example with sample dataset
The sample dataset was relocated after it was written, so allow_moved_paths is required here:
SELECT count(*)
FROM iceberg_scan('s3://us-prd-motherduck-open-datasets/iceberg/lineitem_iceberg',
allow_moved_paths = true);
Writing individual Iceberg tables
COPY ... TO with FORMAT iceberg writes a query result as a standalone Iceberg table at an object-store path, without a catalog:
COPY (SELECT * FROM my_table)
TO 's3://my-bucket/my-iceberg-table' (FORMAT iceberg);
The result can be read back with iceberg_scan (no allow_moved_paths needed) and by other Iceberg readers.
This write path has important caveats:
- Each
COPYcreates a brand-new table. Writing to a path that already contains an Iceberg table replaces it: the previous snapshot history is lost and the previous data files are left orphaned in thedata/prefix. It is not an append or an Iceberg-transactional overwrite. PARTITION_BYis not applied. The table is written with an empty partition spec regardless of anyPARTITION_BYclause.
For transactional writes with snapshot history, appends, and partitioning, write through an attached Iceberg REST catalog instead.
