Skip to content

maint: prepare duckdb 2.0 compatibility - #38

Draft
smithclay wants to merge 9 commits into
mainfrom
compat/duckdb-2.0
Draft

smithclay wants to merge 9 commits into
mainfrom
compat/duckdb-2.0

Conversation

@smithclay

@smithclay smithclay commented Aug 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • track DuckDB 2.0 ("cyanoptera") for 2.0 compatibility work, now at the branch tip fc9d6e9 (rebased on main @ c3dff30)
  • update extension code for changed DuckDB C++ APIs: identifiers, vectors, table outputs, fallible scalar functions, and (new on the tip) FunctionSignature-based table-function parameters and the reshaped QueryResult
  • fix static extension linking, which changed on the tip and left the daemon without parquet/json without any build error
  • update Quack docs to use DuckDB 2.0 CONNECT for ordinary server-side SQL, keeping quack_query as a one-shot fallback

DuckDB 2.0 tracking

There is still no v2.0.0 tag (latest release is v1.5.6), so the v2.0-cyanoptera branch tip is the newest 2.0 ref:

  • duckdb submodule → fc9d6e9 (tip of v2.0-cyanoptera, 2026-10-02), up from 3a604ab (1233 commits)
  • extension-ci-tools → 969bc76. ci-tools now has its own v2.0-cyanoptera branch, so the submodule, .gitmodules, and the reusable-workflow @ref/ci_tools_version follow it instead of main, which will move on to 2.1
  • duckdb_version in MainDistributionPipeline.yml stays v2.0-cyanoptera

duckdb_version is both the git ref and the version stamped into artifacts. A branch name is not a semver, so the stamped version has no published extensions to resolve. Pin to the tag once 2.0.0 lands.

What changed on the tip (3a604ab → fc9d6e9)

APIs

  • TableFunction::named_parameters / arguments are gone; parameters are a Python-style FunctionSignature. otlp_serve/otap_serve now declare their options as a typed **kwargs schema (WithTypedKwargs, the same mechanism read_csv uses). An omitted option is not passed, so bind defaults behave exactly as before. Side effect: duckdb_functions() now lists those options by their real names instead of colN.
  • QueryResult lost GetValue() and QueryResultType. query --format box renders through QueryResult::ToBox, which keeps main's BoxRenderer output rather than ToString()'s debug form, and the seal harness reads its count through Fetch().

Linking (silent breakage)

  • A statically linked extension now registers itself only if the program roots it via link_extension_libraries. That function reads the link set DuckDB resolves after its extension loop, and our CMakeLists.txt runs inside that loop, so the daemon and both harnesses linked no parquet/json/core_functions at all. Everything compiled, then duckdb-otlp convert --to x.parquet failed with Copy Function "parquet" is not in the catalog. otlp_link_duckdb_extensions defers the call to the end of configure.
  • Loading an extension config no longer implies linking it into the shell/unittest. extension_config.cmake now calls duckdb_extension_statically_link(otlp); without it, build/release/duckdb had no otlp_serve.

Validation

On fc9d6e9 (macOS arm64):

  • GEN=ninja make release: clean
  • make test: 23/23 files, 387 assertions
  • otlp_seal_harness: 11 cases, 204 assertions
  • otlp_arrow_harness: 2 cases, 6 assertions
  • pytest test/server: 247/247 (see below)
  • Daemon by hand (--mode parquet): OTLP/HTTP protobuf, NDJSON, and gzip ingest → 202; shutdown seals; query reads back through the Parquet views; export writes NDJSON
  • DuckLake seal end to end: no ducklake is published for this build, and the v2.0.0-alpha39998 binary null-derefs on load (ABI mismatch: it was built from eb7b95df82). So I built ducklake from source at the commit DuckDB's 2.0 branch pins (e8924cf) and ran otlp_serve(catalog := 'lake'): POST logs/traces/metrics → otlp_flush sealed all rows in one snapshot, 0 seal failures, otlp_stop dropped 0
  • The clang-format 11.0.1 pre-commit hook could not run locally (x86-only wheel, no Rosetta), so the code-quality job is the formatting check for this push

daemon-compile: fixed by building ducklake into the daemon

This used to be a known failure. The default local-ducklake mode ran INSTALL ducklake at startup, and no ducklake is published for a 2.0 dev build. The v2.0.0-alpha39998 binary null-derefs against the tip. No version string fixes this, because the real problem is that the daemon only works when a published extension happens to match its ABI.

Now ducklake is linked into the daemon instead (a4d5b615):

  • DUCKDB_OTLP_BUILT_IN_EXTENSIONS links extensions taken from DuckDB's own pinned configs (duckdb/.github/config/extensions/<name>.cmake: the commit and patches DuckDB builds against this source), so the ABI matches by construction.
  • make server-release (Docker and both daemon CI jobs) sets it to ducklake and first builds CRoaring, ducklake's only dependency, into build/deps. The extension distribution builds are unchanged.
  • Core(name) reports BUILT_IN when LinkedExtensionRegistry says the binary links it, so no INSTALL/LOAD is emitted. Because this asks the binary itself rather than a build flag, the two cannot drift.
  • The image stops priming ducklake.
  • The DuckLake read-back in test_init_sql_adds_a_parquet_destination_next_to_the_ducklake_catalog now goes through the binary under test. The host's duckdb==1.5.6 cannot open a 2.0-format database (storage version 69).

Verified:

  • The local-ducklake daemon with an empty extension cache ingests, seals and reads back, with zero downloads.
  • pytest test/server passes 247/247 on macOS and against the Docker-built linux/arm64 daemon-export binary.

Still open

  • quack is not built in. On 2.0 it draws its tokens from the writable crypto module that only httpfs (OpenSSL) provides. Building it in means building httpfs in, which goes against the image's no-libssl design. Until 2.0.0 ships, Quack, docker-smoke, and the cloud modes (httpfs/aws/iceberg/postgres/gcs) still need published extensions, and the Dockerfile still pins DUCKDB_VERSION to v1.5.6 for them.
  • The same gap is why pages.yml, the host duckdb CLI downloads, duckdb==1.5.6 in the server tests, and the Makefile's WASM VERSION_FIELD all stay on the 1.5 line.

Notes

  • src/otlp_column_promote.cpp includes duckdb/main/query_result.hpp; 2.0 removed materialized_query_result.hpp.
  • otlp_server_list emits one row per listener using SetChildCardinality.
  • windows_amd64 builds against DuckDB 2.0 here for the first time, so watch it on the next run. The deferred cmake_language(DEFER) call only applies inside the non-Windows daemon/harness block.
  • AGENTS.md documents both 2.0 changes: function metadata now comes from the function's own FunctionSignature (AddPositionalOnly is how to name a reader's argument), and embedding executables must link DuckDB's extensions through otlp_link_duckdb_extensions.

@smithclay
smithclay force-pushed the compat/duckdb-2.0 branch 2 times, most recently from c1493e8 to 828b73c Compare September 19, 2026 18:34
smithclay pushed a commit that referenced this pull request Sep 21, 2026
The last docs pass said VARIANT buys smaller files but slower queries. That
is true of the DuckDB this pins and false of the one it is heading for, so
the docs now state it per version.

Built the feature against PR #38's DuckDB 2.0 branch (v2.0-cyanoptera,
3a604ab) in a throwaway tree and ran the same queries over the same Parquet
files on both engines, 200k log records:

                                  1.5 JSON -> VARIANT    2.0 JSON -> VARIANT
  string key, repeated bags         3ms -> 445ms           28ms -> 3ms
  string key, per-record bags      45ms -> 256ms           52ms -> 3ms
  GROUP BY an attribute            34ms -> 483ms           30ms -> 4ms
  numeric key                      59ms -> 317ms           53ms -> 124ms

2.0's shredded execution and extraction pushdown read the one shredded
subcolumn instead of rebuilding the bag, which is what flips the result:
the idiom does not matter (variant_extract, the [] shorthand and a VARIANT
comparison all measure the same), and touching the column without
extracting anything drops from 468ms to 130ms. Numeric keys do not get the
same treatment yet. Ingest changes little: on 2.0 the posted-throughput
penalty disappears into the noise and the seal penalty drops from +42% to
+25%, with the same -40% on disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHzXbDhmfExBgUGrwBybbj
smithclay and others added 6 commits October 3, 2026 11:17
Move the DuckDB submodule from an old `main` commit (8616efa, 2163 commits
behind) to the tip of `v2.0-cyanoptera` (3a604ab), the feature-frozen 2.0
release branch. No v2.0.0 tag exists yet — the branch is the newest 2.0 ref
available — so `.gitmodules` now tracks it instead of `main`, and the CI
`duckdb_version` inputs point at the same ref. `extension-ci-tools` moves to
its `main` tip, which is what builds against the 2.0 branch.

Rebasing onto main pulled in `otlp_column_promote.cpp`, which was added after
this branch was cut and still included `duckdb/main/materialized_query_result.hpp`.
DuckDB 2.0 removed that header along with `MaterializedQueryResult`;
`Connection::Query` now returns `unique_ptr<QueryResult>`. The file only uses
`HasError()`/`GetError()` from `BaseQueryResult`, so the include moves to
`duckdb/main/query_result.hpp`.

Also resolved during the rebase: `otlp_server_list` now emits one row per
listener (main's N-listener refactor) using `SetChildCardinality`, and the
Quack docs component keeps its `token` prop while using 2.0's CONNECT form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nRtuxxexQqGerw5pqAVB9
`duckdb_version` is both the git ref checked out and the version string
stamped into the built binaries. `v2.0-cyanoptera` is a branch name, not a
semver, so DuckDB's CMake leaves it unparsed and the resulting binaries
resolve extensions under extensions.duckdb.org/v2.0-cyanoptera/, which does
not exist. Only the published v2.0.0-alpha39998 has extension binaries, and
there is no git ref for it.

Harmless for these two jobs -- no test in test/sql INSTALLs an extension --
but it is why the Docker/daemon path, the host duckdb CLI downloads and the
pages.yml pins stay on v1.5.5 for now. Record that in the workflow so the
next person does not have to rediscover it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nRtuxxexQqGerw5pqAVB9
Rebasing onto main (511c5b2) pulled in the CLI (#68), the `--init-sql` /
shutdown-seal work (#70), the config-surface collapse (#73) and the
`duckdb_functions()` metadata pass (#76). Three of those touch APIs 2.0
changed, so they need the same treatment the rest of this branch already got:

- `src/server/commands.cpp`: DuckDB 2.0 removed `MaterializedQueryResult`
  along with `duckdb/main/materialized_query_result.hpp`, and folded
  `RowCount()` / `ToString()` / `GetValue()` into `QueryResult`. The three
  `Cast<MaterializedQueryResult>()` sites call those directly now, and
  `PrintBox` takes a `QueryResult &`. `BaseQueryResult::type` is private in
  2.0, so the "did this statement produce a table?" check goes through
  `GetResultType()`.
- `src/otlp_start_stop.cpp`: the no-argument `otlp_stop()` overload (#70)
  pages its rows out through two more `SetCardinality` calls, which 2.0
  renamed to `SetChildCardinality`. Its bind signature takes
  `vector<Identifier> &names` like every other bind on this branch.
- `src/otlp_uri.cpp`: `otlp_uri_parser` is now registered through
  `OtlpDocumented` (#76). The function is built first so `SetFallible()`
  still applies — 2.0 requires it of a scalar function that throws — and
  then handed to the wrapper.

`src/server/main.cpp` drops the now-unused `database_manager.hpp` include,
and AGENTS.md's `--init-sql` note no longer points at `SetDefaultDatabase`,
which this branch removed.

Verified: every extension, daemon and harness translation unit compiles
against the 2.0 headers (`-fsyntax-only` over the full compile database),
and clang-format 11.0.1 is clean on every changed file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgQrTbecUyFCwpr4mumUwR
Two behaviors main relies on changed in 2.0. Both surfaced as real test
failures after the rebase, not as compile errors.

`duckdb_functions()` parameter names (#76). A `FunctionDescription`'s
`parameter_names` used to win outright; in 2.0 they are only a fallback,
consulted when the count of the function's *own* signature names does not
match the rendered parameter list (`GetParameterNames` in
`duckdb_functions.cpp`). A `ScalarFunction` can carry real names, so
`otlp_uri_parser` is now built from a named `FunctionParameter` and reports
`listen_uri` again instead of `col0`. A `TableFunction` cannot -- it takes a
bare `vector<LogicalType>` and its extractor emits `col0`, `col1`, ...
unconditionally -- so on 2.0 every reader and control-plane table function
reports `colN` whatever its description says. `function_metadata.test`'s
placeholder assertion is scoped to the scalar function accordingly, with the
mechanism written down; the readers keep their real `parameter_names` so they
document the argument and take effect if upstream restores the override.

`query`'s echo of the user's own SQL (#68). 2.0 reimplemented
`Connection::ExtractStatements` on top of the lazy statement iterator, which
-- unlike `ClientContext::ParseStatements`, still private -- does not run
`ProcessError`. A parser error therefore arrives stripped of the
"LINE n: ... ^" context that `RunDataCommand`'s `keep_sql_context` branch
exists to pass through, and `duckdb-otlp query "SELCT 1"` reported a bare
`syntax error at or near "1"`. The parse is wrapped so the statement the user
wrote is put back on the message.

Verified against 3a604ab: `make test` 23/23, `otlp_arrow_harness` 6
assertions, `otlp_seal_harness` 168 assertions, `pytest test/server` 244
passed. The 2 remaining server-test failures are this branch's documented
blocker -- `INSTALL ducklake` 404s because DuckDB publishes no 2.0 extension
binaries yet -- and are unrelated to these changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgQrTbecUyFCwpr4mumUwR
Still no v2.0.0 tag, so this tracks the newest 2.0 ref: duckdb 3a604ab ->
fc9d6e9 (1233 commits). extension-ci-tools now has its own v2.0-cyanoptera
branch, so the submodule, .gitmodules and the reusable-workflow refs follow it
instead of `main`, which will move on to 2.1.

API changes on the tip:

- Table functions declare parameters through a Python-style FunctionSignature;
  `TableFunction::named_parameters` and `arguments` are gone. otlp_serve /
  otap_serve declare their optional options as a typed "**kwargs" schema
  (WithTypedKwargs, the mechanism read_csv uses): an option the call leaves out
  is not passed, so the bind keeps its own defaults exactly as before. A side
  effect is that duckdb_functions() now reports the options by their real names.
- QueryResult lost GetValue() and QueryResultType. `query` renders through
  QueryResult::ToBox (the BoxRenderer main switched to, so `--format box` stays
  a real box, not ToString()'s debug form); the seal harness reads its count
  through Fetch().

Linking changed, and it broke the daemon silently -- it compiled, then
`duckdb-otlp convert --to x.parquet` failed with `Copy Function "parquet" is
not in the catalog`:

- A statically linked extension now registers itself only if the program roots
  it, via link_extension_libraries. That reads the link set DuckDB resolves
  after its extension loop, and this CMakeLists runs inside the loop, so the
  daemon and both harnesses linked no parquet/json/core_functions at all.
  otlp_link_duckdb_extensions defers the call to the end of configure.
- Loading an extension config no longer implies linking it into the shell and
  unittest; extension_config.cmake asks with duckdb_extension_statically_link.
  Without it build/release/duckdb had no otlp_serve.

Verified against fc9d6e9: make test 23/23 (387 assertions),
otlp_seal_harness 204 assertions, otlp_arrow_harness 6, pytest test/server
245 passed. The 2 failures are the known blocker (no published ducklake for a
2.0 dev build). DuckLake seal verified separately against a ducklake built
from source at the commit DuckDB's 2.0 branch pins (e8924cf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
smithclay and others added 3 commits October 3, 2026 11:43
…king

The function-metadata section still described v1.5: options in an
unordered_map rendered as colN. On the 2.0 tip duckdb_functions() renders
names from the function's own FunctionSignature, typed **kwargs options are
listed by name in declaration order, and a bare-LogicalType positional is
named colN by AddParameter(type), not by the extractor. Point at
AddPositionalOnly as the way to name a reader's argument.

The Build System section gains the two things a fresh session would trip
over: the submodules and workflow refs track v2.0-cyanoptera until a tag
exists, and an embedding executable must link DuckDB's extensions through
otlp_link_duckdb_extensions, since a direct link_extension_libraries call
links nothing yet still builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
daemon-compile could not pass on a DuckDB 2.0 development build: the
default local-ducklake mode INSTALLs ducklake at startup, no ducklake is
published for a dev build, and the 2.0 alpha's binary null-derefs on load
against the branch tip. No version string fixes that, because the problem
is the coupling itself -- the daemon only worked when a published extension
build happened to match its ABI.

So the daemon stops downloading the extension its default mode needs:

- DUCKDB_OTLP_BUILT_IN_EXTENSIONS (CMake list) links extensions into
  DuckDB. extension_config.cmake takes each from DuckDB's own pinned config
  (duckdb/.github/config/extensions/<name>.cmake: the commit and patches
  DuckDB builds against this source), so the ABI matches by construction.
- `make server-release` -- the path Docker and both daemon CI jobs take --
  sets it to ducklake and first builds CRoaring, ducklake's one dependency,
  into build/deps, since those builds run without vcpkg. v4.7.2 rather
  than vcpkg's 4.4.2, whose package config CMake 4 rejects. The extension
  distribution builds are unchanged.
- Core(name) reports BUILT_IN when LinkedExtensionRegistry says the binary
  links it, so no INSTALL/LOAD is emitted and the banner says "built in".
  Asking the binary rather than a build flag means the two cannot drift.
- The image stops priming ducklake.

quack stays a runtime install: on 2.0 it draws its tokens from the
writable crypto module only httpfs (OpenSSL) provides, so building it in
means building httpfs in, against the image's no-libssl design.

test_init_sql_adds_a_parquet_destination_next_to_the_ducklake_catalog read
the daemon's catalog back with the host's duckdb==1.5.6, which cannot open
a 2.0-format database (storage version 69). It now reads it back through
the binary under test (`duckdb-otlp query`), which is still a real DuckLake
read and no longer depends on the host's DuckDB version.

Verified: local-ducklake daemon with an empty extension cache ingests,
seals and reads back with zero downloads; pytest test/server 247/247 both
on macOS and against the Docker-built linux/arm64 daemon-export binary;
make test 23/23, seal harness 204, arrow harness 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 2.0

clang-format 11 wanted two whitespace changes (otlp_start_stop.cpp,
server_config.cpp).

The daemon smoke failed because Quack cannot load in a 2.0 daemon: no
extensions are published for the 2.0 development build, and the image's
offline cache holds v1.5.6 builds whose init crashes. smoke_test.py gains
--verify restart, which seals by SIGTERM (graceful otlp_stop), requires a
clean exit, and reads the count back with the image's own query
subcommand over the same /data volume. Both CI smoke jobs use it and no
longer install a host duckdb CLI. The default stays --verify quack.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants