Skip to main content

What the manager does to the database

DuckLake's own DDL creates the catalog; the manager then shapes it for SQL Server. Everything below is idempotent and stamped with a shape version, so a catalog created by an older build is brought up to date on its next attach, and a catalog whose tables were dropped and recreated is shaped again.

Primary keys

The mssql extension builds a row identity out of a table's primary key, and without one it refuses UPDATE and DELETE. DuckLake's commit batch updates ducklake_table_stats on every write past a table's first, and expiry, cleanup and compaction delete from a dozen tables — so every table DuckLake updates or deletes from gets a primary key (ducklake_table_stats(table_id), ducklake_data_file's statistics on (data_file_id, column_id), and so on). Key columns are made NOT NULL; the tables keyed on a name (ducklake_metadata, the tag tables) get a VARCHAR(200) key column, inside SQL Server's 900-byte index limit.

Strings and their collation

DuckLake's catalog stores its strings — names, paths, and the per-file minimum and maximum values it prunes with — as VARCHAR(MAX) under Latin1_General_100_BIN2_UTF8, never NVARCHAR. Two reasons: UTF-8 is what DuckDB writes, so nothing is transcoded; and BIN2 is DuckDB's byte order, so a filter the server answers on a min_value/max_value column compares the way DuckDB computed the statistics. A linguistic collation would prune wrongly and drop rows from a result. The collation is explicit on every column because a database's own is usually a legacy CI_AS one. This is why the server needs the UTF-8 collations of SQL Server 2019 (Requirements).

Indexes

Neither the PostgreSQL nor the SQLite manager adds indexes; on SQL Server the difference measured 15x on the reads that matter:

indexserves
(table_id, begin_snapshot, end_snapshot) on ducklake_data_file and ducklake_delete_filethe visibility condition every read carries, at the current snapshot and at older ones alike
filtered WHERE end_snapshot IS NULL on ducklake_table, ducklake_column, ducklake_viewthe catalog load, which asks for the current state only
(table_id, column_id) on ducklake_file_column_statsthe per-column statistics a filtered read prunes with — a row per file per column, the largest table in the catalog
(table_id, partition_key_index, partition_value) on ducklake_file_partition_valuepartition pruning; partition_value is bounded to VARCHAR(200) so it can be a key at all

The inlined-data tables

Rows below the inlining limit go into a ducklake_inlined_data_<table>_<schema version> table the manager creates with T-SQL types: BIT, SMALLINT, INT, BIGINT, DECIMAL(p, s), DATE, TIME(6), DATETIME2(…), DATETIMEOFFSET(6), UNIQUEIDENTIFIER, VARBINARY(MAX) — and VARCHAR(MAX) for what SQL Server cannot hold exactly (Writing). Each has a primary key on (row_id, begin_snapshot), so DuckLake's inlined UPDATE and DELETE work; the ducklake_inlined_delete_<table> tables that hold inlined deletions of file-backed rows are keyed on (file_id, row_id, begin_snapshot) the same way, so a flush can clear them. The table is created in the commit that creates the lake table, outside the transaction, so that the mssql extension's metadata cache can see it before the first INSERT needs it.

Forced parameterization

The last step is one statement outside the catalog's own tables:

ALTER DATABASE CURRENT SET PARAMETERIZATION FORCED;

Every query DuckLake and the mssql extension send carries its literals in the text — a table name, a table_id, a snapshot id — and SQL Server caches plans by text, so without the option each distinct value is a plan of its own, compiled on first use: about 35 ms per table the first time it is touched. With it the server parameterizes the literals itself and one plan serves every value. On the 1000-table benchmark this took a quarter off the total and made the first write into each table two to three times faster (Performance).

It is database-wide, so it has an opt-out — SET mssql_ducklake_forced_parameterization = false before the attach that shapes the catalog — and it is best-effort: a login allowed to create the catalog's tables but not to alter the database gets a working catalog without it, and a warning in duckdb_logs() naming the statement for someone who may run it. It is applied when the catalog is shaped, not on every attach: set it back and it stays back.

The shape stamp

The version of all of the above is recorded as an extended property (mssql_ducklake_shape) on the catalog's ducklake_metadata table — per catalog, invisible to DuckLake's queries, and gone the moment the catalog's tables are. Attaching reads it in one query; the DDL only runs when the stamp is missing or older than this build's shape version.