Skip to main content

Register Delta Lake tables into a DuckLake catalog without copying the underlying Parquet data

Project description

delta2ducklake

Register a Delta Lake table into a DuckLake catalog without copying or rewriting the underlying Parquet files. Reads the Delta transaction log directly (JSON commits + checkpoints), no dependency on the deltalake Python package.

Status: beta. Published on PyPI.

Supported

  • Full (copy_table) and incremental (sync_table) conversion, including table/file statistics and Hive-style partitioning
  • Delta column mapping (columnMapping.mode = name / id)
  • Deletion vectors, converted to DuckLake positional delete files
  • A standalone refresh_stats() utility to (re)compute stats for any DuckLake table's columns (useful since Delta's dataSkippingNumIndexedCols often leaves trailing columns with none)
  • Catalog backends: a local DuckDB database file (DuckLake's own native format), SQLite, PostgreSQL (including Entra ID/Azure AD token auth for Azure Database for PostgreSQL)
  • Storage backends: local filesystem, Azure Blob/ADLS Gen2 (delta2ducklake[azure])

See docs/IMPLEMENTATION.md for design notes, protocol details pinned down against real fixtures, and known limitations (e.g. DuckDB's own ducklake reader can't materialize partition column values for tables whose physical layout isn't Hive-style).

Ships an Agent Skill at delta2ducklake/skills/delta2ducklake/SKILL.md (included in the installed package) so AI coding assistants pick up correct usage automatically once this package is a project dependency.

Install

uv add delta2ducklake
uv add "delta2ducklake[azure]"      # if your Delta table or Postgres catalog lives on Azure

Quick start

A DuckLake catalog needs to be bootstrapped once (creates its metadata schema), then tables are registered into it and kept up to date with sync_table() — it creates the table on first call and updates it on every call after, so it's safe to call unconditionally (e.g. from a recurring job). The simplest catalog is a local DuckDB database file — DuckLake's own native format:

from delta2ducklake.ducklake.bootstrap import bootstrap_catalog
from delta2ducklake.ducklake.catalog import DuckDBCatalogConfig
from delta2ducklake.convert import sync_table

catalog = DuckDBCatalogConfig("/abs/path/to/catalog.ducklake")
bootstrap_catalog(catalog, data_path="/abs/path/to/ducklake_data/")  # one-time; safe to re-run

# Registers "my_table" if it doesn't exist yet, or brings it up to date if it does.
sync_table("/path/to/my_delta_table", catalog, "my_table")

Use copy_table() instead if you specifically want a hard failure when the table already exists, rather than an update.

DuckDBCatalogConfig also accepts an already-open duckdb.DuckDBPyConnection instead of a path (e.g. for in-memory testing, or when you already manage a connection to the catalog file) — connect() reuses it as-is and close() leaves it open, since the caller owns its lifecycle. bootstrap_catalog() still needs a real path (it ATTACHes from a separate connection), so bootstrap first, then wrap a connection to the same file if you want to reuse one.

Or use a SQLite file:

from delta2ducklake.ducklake.catalog import SQLiteCatalogConfig

catalog = SQLiteCatalogConfig("/abs/path/to/catalog.sqlite")
bootstrap_catalog(catalog, data_path="/abs/path/to/ducklake_data/")

Use absolute paths. DuckDB's own ducklake extension pins a catalog to the exact DATA_PATH string it was first bootstrapped with and rejects any later ATTACH whose DATA_PATH doesn't match that exact string — a relative path resolves differently depending on the current working directory, so bootstrap_catalog()/ATTACH calls made from different directories (or a cron job vs. a shell) will spuriously conflict unless every caller passes the identical absolute path.

For Postgres, swap in a DSN:

from delta2ducklake.ducklake.catalog import PostgresCatalogConfig

catalog = PostgresCatalogConfig("host=localhost dbname=mydb user=me password=...")
bootstrap_catalog(catalog, data_path="/abs/path/to/ducklake_data/")  # or an Azure URL, see below

For Azure Database for PostgreSQL, authenticate with an Entra ID (Azure AD) token instead of a static password by setting entra_user (needs delta2ducklake[azure]; mirrors the pattern used by bmsuisse/pgdevkit). A fresh token is fetched on every connect()/bootstrap, since Entra tokens expire:

catalog = PostgresCatalogConfig(
    "host=myserver.postgres.database.azure.com dbname=mydb",
    entra_user="app@mydb",       # omit user/password from the DSN — these are supplied for you
    managed_identity=False,      # True to use ManagedIdentityCredential instead of DefaultAzureCredential
)

There's also experimental support for Quack, DuckDB's client-server RPC protocol, letting a remote DuckDB instance act as the catalog:

from delta2ducklake.ducklake.catalog import QuackCatalogConfig

catalog = QuackCatalogConfig("localhost:9494", token="...")  # matches quack_serve(...) on the server
bootstrap_catalog(catalog, data_path="/abs/path/to/ducklake_data/")

Known limitation: as of DuckDB v1.5.5, tables reached via Quack's remote attach only support INSERT/SELECTUPDATE and DELETE both fail with Binder Error: Can only update/delete from base table (confirmed against a real quack_serve() instance). Since copy_table() writes its bookkeeping via DELETE+INSERT and sync_table() retires removed files via UPDATE, neither currently works against a Quack-backed catalog — only bootstrap_catalog() and raw read-only queries do. This is a limitation of the experimental protocol itself, not something delta2ducklake can work around; it should start working with no code changes here once Quack's DML support matures.

Then query the result with DuckDB directly:

INSTALL ducklake;
ATTACH 'ducklake:sqlite:/abs/path/to/catalog.sqlite' AS dl (DATA_PATH '/abs/path/to/ducklake_data/');
SELECT * FROM dl.my_table;

Backfill or recompute stats for specific columns of any DuckLake table (not just ones this package created):

from delta2ducklake.ducklake.stats_refresh import refresh_stats

refresh_stats(catalog, "my_table", columns=["a_wide_table_column_delta_never_indexed"])

CLI

The same operations are available as delta2ducklake <command> (installed as a console script):

delta2ducklake bootstrap --catalog sqlite:catalog.sqlite --data-path ./ducklake_data/
delta2ducklake copy   /path/to/my_delta_table --catalog sqlite:catalog.sqlite --table my_table
delta2ducklake sync   /path/to/my_delta_table --catalog sqlite:catalog.sqlite --table my_table
delta2ducklake refresh-stats --catalog sqlite:catalog.sqlite --table my_table --columns a,b

# Postgres: --catalog "postgres:host=localhost dbname=mydb user=me password=..."

# Azure Postgres with an Entra ID token instead of a static password:
delta2ducklake copy /path/to/my_delta_table \
  --catalog "postgres:host=myserver.postgres.database.azure.com dbname=mydb" \
  --entra-user app@mydb --table my_table

Run delta2ducklake <command> --help for the full option list (e.g. --schema, --version for time travel).

Development

uv sync --all-extras --all-groups
uv run pytest
uv run ruff check src tests
uv run ty check src/delta2ducklake

Postgres-backed tests are skipped unless DELTA2DUCKLAKE_TEST_PG_DSN is set to a libpq keyword=value connection string for a database the test user can create/drop databases in (each test provisions and tears down its own isolated database):

export DELTA2DUCKLAKE_TEST_PG_DSN="host=localhost port=5432 user=postgres password=..."
uv run pytest

CI (.github/workflows/python-test.yml) runs lint, ty check, and the full test suite (including Postgres, against a postgres: service container) on every push/PR. Publishing (.github/workflows/python-publish.yml) runs on GitHub Release (or manual dispatch) and pushes to PyPI via Trusted Publishing (OIDC, no stored token) — set that up once in the PyPI project's settings before cutting a release.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

delta2ducklake-0.2.0.tar.gz (40.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

delta2ducklake-0.2.0-py3-none-any.whl (50.8 kB view details)

Uploaded Python 3

File details

Details for the file delta2ducklake-0.2.0.tar.gz.

File metadata

  • Download URL: delta2ducklake-0.2.0.tar.gz
  • Upload date:
  • Size: 40.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for delta2ducklake-0.2.0.tar.gz
Algorithm Hash digest
SHA256 b849b3e3581f26ee5717845677733e94b5f8cdd4525f1ad324e6bbc8b2c54b58
MD5 aeaf5342c3b98722fcbd2343a36d4301
BLAKE2b-256 b9ed95c3d549b1a5f9d8175e6ccb587d3593e3a831e34fc942cde93d4e052672

See more details on using hashes here.

Provenance

The following attestation bundles were made for delta2ducklake-0.2.0.tar.gz:

Publisher: python-publish.yml on bmsuisse/delta2ducklake

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file delta2ducklake-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: delta2ducklake-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 50.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for delta2ducklake-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b0145d944ae32a6b043ab0eda4b13b7b3f12716ef63a2f8a4f6218615bbae6d7
MD5 94a63dbea4e24a7dcc2a77cf32c38c8e
BLAKE2b-256 95d38e4f3bcf2c157d9325c65289c28b44938cf72c98421744f27288321ceaa3

See more details on using hashes here.

Provenance

The following attestation bundles were made for delta2ducklake-0.2.0-py3-none-any.whl:

Publisher: python-publish.yml on bmsuisse/delta2ducklake

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page