Skip to content

Sitting Duck v1.18.1

Release date: 2026-11-05

A patch release: one new parameter, and a set of documented examples that could not do what they said.

This release requires DuckDB 2.0, unchanged from v1.18.0. The duckdb submodule pin is still d54cbbe3 (v2.0.0). No API changed shape, no output value changed, and no existing query returns anything different.

New

ast_select() takes a peek parameter (#261).

-- peek is NULL, as it has always been
SELECT name, peek FROM ast_select('src/app.py', '.func');

-- peek carries each function's full text
SELECT name, peek FROM ast_select('src/app.py', '.func', peek := 'full');

The level is forwarded verbatim to read_ast(), so it accepts the same values. The default is unchanged: ast_select still does not extract peek unless the selector carries a [peek...] attribute filter, and an explicit peek := now wins over that auto-materialization.

This is deliberately opt-in, which is not what #261 asked for. That issue reported peek being "dropped on the way out", with "only the projection lost". Measured, nothing was projected away: ast_select parsed with peek := 'none+schema', which keeps the column in the schema as all-NULL without computing it, so peek was never extracted. Its own evidence shows the mechanism — .str returns 28 rows with 0 non-null peek, while .str[peek*=SELECT] returns 3 with 3, because asking for the filter is what turns extraction on.

The reason not to simply switch it on, measured over src/*.cpp (27 files, 464 KB of source, 93,675 nodes):

peek level extracted text vs source
none+schema (default) 0 B —
smart 1.23 MB 2.6x
full 4.20 MB 9.0x

Extraction time is not measurable above run-to-run noise — 0.78-0.84 s at every level, with the levels interleaved so the noise has its own control. So peek's cost is memory, and it grows with tree depth rather than file size, because every ancestor of a match carries a copy of the matched region. Turning that on for every caller to fix a NULL column is the hazard class of tracker/bugs/055, where an unbounded documentation example took a 125 GB host down.

peek := 'smart' is not a cheap substitute for 'full': measured, it returns only the first line, so it gives a signature rather than a body.

Fixes

The CSS selector tutorial's capstone now answers its own question. It asks "Which methods in UserService call the database without error handling, and what SQL do they use?" and selects peek, which came back NULL. It now passes peek := 'full', and the third column carries each method's body — create_user with its INSERT INTO, delete_user with its DELETE FROM.

ast_unparse() was documented as a function that does not exist, in four files. The documented form was a scalar taking an AST table with three formatting parameters:

ast_unparse(ast_table, [indent_size := 4, use_tabs := false, newline := '\n']) -> VARCHAR

Measured against duckdb_functions(), ast_unparse is a table macro taking (path, preset). So the signature was wrong in three ways at once — a scalar return for a table macro, an AST table for a function that takes a file path, and three named parameters that do not exist — and every example built on it failed to bind:

Binder Error: Function "ast_unparse" is a table function but it was used as a
scalar function.

docs/reference/unparse.md already documented it correctly, so the fix aligns the other files to it rather than inventing a signature. Corrected in docs/reference/functions.md, docs/index.md, README.md and docs/how-to/code-transformation-and-unparsing.md; the in-memory case is ast_unparse_from(ast_table, preset), which the old examples actually wanted.

Two things that fix turned up:

  • A section documented a feature that does not exist. Indentation and line endings come from a preset (pep8/black, gofmt, prettier, llvm, google, rustfmt), not from per-call options. The replacement shows a real difference rather than asserting one: the default indents go with four spaces, gofmt with a hard tab.
  • The roundtrip example had a second, unrelated defect. Fixing its call shape exposed Binder Error: Table function cannot contain subqueries, because parse_ast((SELECT code FROM unparsed), 'python') cannot bind — so that example could never have run however ast_unparse was spelled. It now passes the text through SET VARIABLE / getvariable(), and asserts the pseudo-identity property instead of printing a join: 18 nodes on both sides, 0 mismatches.

v1.18.0's release notes carried six references to five tracker items, each autolinking to an unrelated issue. They were written #042, #054, #047, #040, #055; GitHub strips the leading zero and autolinks, so #054 pointed at a closed PR titled "Fix native extractors for Java, Swift, and PHP" — close enough to the subject that a reader had no reason to doubt it. Tracker items are now cited by path, as the rest of docs/ does. One of them, #047, was also an unsupported attribution: the DEF_TYPE duplicate-raw-type gate is not tracked as 047 anywhere, and that line now names scripts/check_def_type_tables.py.

Known issues

Unchanged from v1.18.0, and none of them is addressed here.

  • ast_select is slow on DuckDB 2.0 (tracker/bugs/040): 13.05 s to execute and 72.23 s to EXPLAIN on an eleven-node table, planning single-threaded. The cause is upstream, duckdb/duckdb#26036.
  • 116 documented examples read an unbounded tree (tracker/bugs/055).
  • Call-graph pseudo-classes can exhaust memory (#164).
  • Attribute filters inside :has(...) are not supported (#150). The selector engine refuses them with a clear error. The tutorial no longer depends on one.
  • The conformance kit runs nowhere in CI.

Upgrading

Nothing is required. v1.18.1 changes no output value and removes nothing, so a v1.18.0 query returns exactly what it returned before. peek := is additive.

Verification

Measured on the release candidate, not carried over from earlier runs.

  • Test suite, make test_release: All tests passed (5 skipped tests, 9122 assertions in 168 test cases), exit 0. The 5 skips are the runtime-grammar files that need SITTING_DUCK_TEST_GRAMMAR_DIR; 173 .test files exist and 168 ran. v1.18.0 reported 9101 assertions in 167 cases, so this is exactly +1 file and +21 assertions -- the new bug test and nothing else. Measured: test/ differs from v1.18.0's measured tree by that one file.
  • The suite is its own stale-binary control. Every peek := query in test/sql/bugs/261_ast_select_peek.test raises a Binder Error against a binary built before the macro change, because the parameter does not exist there. So 21 passing assertions in that file are positive evidence that the tested binary carries the fix, not merely that nothing failed.
  • Three controls hold the default. .func and .str must still report 0 non-NULL peek out of 20 and 28 rows, and .str[peek*=SELECT] must still report 3 of 3. A change that forced peek on for every caller would turn the first two into 20 and 28 while passing every other assertion in the file.
  • Module ABI gate: OK, 17 strategies (11 dispatched, 6 reserved), 20 traits mappings over 28 per-language adapters, 330 non-NONE declarations in 28 languages, 85 (language, strategy) pairs.
  • Native dispatch gate: OK, 69 known-dead literals of 1380 across 27 languages, exactly as spec/abi/module-abi.yaml records them, and no new ones.
  • DEF_TYPE table gate: OK, 3837 entries across 31 translation-unit roots, every entry closing on its own line, no raw type declared twice in one translation unit.
  • Layout invariance gate: OK, 28 language modules each resolving at the spec's paths, 4039 DEF_TYPE entries across 58 files.

All four gates report the same figures as v1.18.0. That is the expected result rather than a reassuring one: src/ changed only in src/sql_macros/css_selectors.sql and the embedded_sql_macros.hpp generated from it, so a moved figure would have meant something unintended had changed.

Scope, stated precisely: the suite ran at ead87b5 on a clean tree. Measured, no commit after it touches any path under src/ or test/ -- everything after it is documentation and the mkdocs.yml nav entry -- so no measurement here is carried across a code change.

What is not re-measured: the conformance kit's full matrix, which took 52m38s for v1.18.0 and cannot be reached by a SQL-macro parameter, and the peek cost table above, which was measured once over src/*.cpp and is quoted rather than re-run.