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:
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:
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 indentsgowith four spaces,gofmtwith a hard tab. - The roundtrip example had a second, unrelated defect. Fixing its call
shape exposed
Binder Error: Table function cannot contain subqueries, becauseparse_ast((SELECT code FROM unparsed), 'python')cannot bind — so that example could never have run howeverast_unparsewas spelled. It now passes the text throughSET 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_selectis slow on DuckDB 2.0 (tracker/bugs/040): 13.05 s to execute and 72.23 s toEXPLAINon 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 needSITTING_DUCK_TEST_GRAMMAR_DIR; 173.testfiles 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 intest/sql/bugs/261_ast_select_peek.testraises 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.
.funcand.strmust still report 0 non-NULLpeekout of 20 and 28 rows, and.str[peek*=SELECT]must still report 3 of 3. A change that forcedpeekon 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 asspec/abi/module-abi.yamlrecords them, and no new ones. DEF_TYPEtable 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, 4039DEF_TYPEentries 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.