Skip to content

fix(duckdb): run on DuckDB 2.x as well as 1.5.x - #740

Merged
fas89 merged 14 commits into
mainfrom
fix/duckdb-2-compat
Oct 22, 2026
Merged

fas89 merged 14 commits into
mainfrom
fix/duckdb-2-compat

Conversation

@fas89

@fas89 fas89 commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

DuckDB 2.0.0 (2026-10-21) turned main red: 51 tests across Test 3.11–3.14, Coverage and the DuckDB end-to-end legs. pip gives DuckDB 2.0 to every Python 3.11+ install, so forge-cli 0.21.0 breaks there too. This PR makes forge-cli run on DuckDB 2.x and keeps 1.5.x working. The floor stays duckdb>=1.5.0, with no ceiling.

What changed

The compat seam. What differs between the versions lives in one module, fluid_build/providers/_duckdb_compat.py, modelled on fluid_build/_mcp_compat.py:

  • first_row / one_row: every DuckDB fetchone() in fluid_build (18 sites) now reads the result to the end. On 2.0, relations have no fetchone(), and a result read only partway keeps its connection busy. An AST fence test keeps partial reads out of fluid_build.
  • query_relation: gives 1.5's answers on 2.x. It returns None for SQL ending in DDL or DML, where 2.0 hands back a Count relation, a Success relation or an earlier query's rows. SQL that does not parse raises before any of it runs. A comment after the final ; is cut, since otherwise 2.0's aggregate, limit and write fail. Earlier statements are drained with 2.0's complete().
  • relation_view: 2.0 refuses a view over rows it already holds (a PIVOT without an IN list, nextval(), RETURNING), so the rows go to a table instead.
  • Storage pin: on 2.x, files forge-cli creates are written with storage_compatibility_version=v1.0.0. That is storage version 64, the version 1.5 writes, so a 1.5 install can open them.

Postgres and MySQL acquisition sources. 2.0 dropped postgres_scan, so the runner now attaches the source read-only before the sandbox's lock: ATTACH ... (TYPE postgres|mysql, SECRET <alias>, READ_ONLY).

  • The ATTACH names an empty temporary secret. Without it, the attach fails under the sandbox with Unknown secret storage found: 'local_file' on every version. That is why MySQL sources never worked.
  • An enum column is cast to VARCHAR, as postgres_scan typed it, so the schema gate and the landed type stay the same.
  • A Postgres secretRef is now resolved.
  • A setup or connect failure becomes a FAILED run with a run record, with the password masked.

CI. A new duckdb-floor job installs exactly MIN_DUCKDB_VERSION (read from the code) on Python 3.12. It runs the compat, sandbox, MCP-driver and DuckDB-runner tests plus every suite the DuckDB end-to-end job runs. tests/test_ci_duckdb_floor.py fails if that list falls behind.

Upgrade notes and Fixed entries are under [Unreleased] in CHANGELOG.md. There is no version bump.

Borrowed, not invented

Validation

Full suite (-n auto --dist loadscope, plus perf run serially) at a2506ac4. Every failure was rerun at bc3e2b41 (which changes tests only) and on unmodified main (1320619b) in the same venv:

Python / DuckDB passed failures at HEAD that main does not share
3.12 / 2.0.0 26,793 none
3.14 / 2.0.0 26,793 none
3.12 / 1.5.6 26,794 none
3.10 / 1.5.6 26,555 none
  • Fixed in bc3e2b41: the run at a2506ac4 found two test-side breaks. The sandbox fence flagged the connectionless duckdb.tokenize, and a fake connection lacked extract_statements.

  • Fail the same on main:

    • test_duckdb_sandbox_workspace_root.py × 2, which is macOS's /var layout and passes on Linux CI;
    • test_coding_agent_live.py, which needs a live Claude CLI.
  • Passed on rerun: a hypothesis property test (load), and the --help wall-clock budgets, which also pass on main under the same load (load average about 150).

  • Floor: the duckdb-floor test list on duckdb 1.5.0 (Python 3.12): 455 passed, 6 skipped. The 2 failures are the macOS-only /var tests in test_duckdb_sandbox_workspace_root.py, which fail the same on main on macOS and pass on Linux CI.

  • Live, through the fluid CLI, against Postgres 16 and MySQL 8.4 containers:

    • examples/source-aligned-postgres-duckdb (validate, apply --mode amend-and-build with 5 rows, verify --strict) passes on 2.0.0, 1.5.0 and 1.5.6 (Python 3.10). main on 2.0 fails with Table Function with name postgres_scan does not exist!.
    • The live Postgres and MySQL round trips pass on all three. A wrong password fails the build with the password masked in the output, the logs and the run record.
    • A Postgres enum column against a declared VARCHAR: exit 1 at 084f5b87 (mood/type_changed→fail), exit 0 here on both versions.
  • Sandbox: the attack battery (a file outside the allowlist, ./.., a symlink, ATTACH after the lock, functions of extensions that were not loaded) refuses the same things on 1.5.0, 1.5.6 and 2.0.0. 2.0's sqlite scanner is now bounded by the allowlist, so it is stricter.

  • Mutation:

    • Round 2: 15/15 killed.
    • Round 3: 7/7 killed (parse-error parity, comment trim, view fallback twice, enum cast wiring and array suffix, fingerprint attach). Narrowing the secretRef catch is killed too.
    • The floor guard catches a dropped e2e file and a dropped version-branching file.
  • Gates: ruff, black 24.10.0, the strict-mypy allowlist, import-linter, the cold-path module budget, detect-secrets on the changed files (CI's exact command), and the license-header writer.

Reviews

  • /security-review: three rounds; no vulnerabilities. The latest round covers 9217f6ec..a2506ac4 with probes: hostile Postgres column names and enum labels, 41k tokenizer and fuzz inputs, the relation_view fallback under the sandbox, and secretRef error text through every output path.
  • World-class inspection, round 2 (docs, codebase, tests, architecture): every finding caused by this change is fixed in 9217f6ec, 084f5b87 and a2506ac4. That includes the detect-secrets hit that would have failed Lint, setup errors escaping the run record, the floor job's coverage, and the CHANGELOG overclaims.
  • Defect scan (security, logic, runtime and cross-file hunters): confirmed defects fixed (secretRef backend errors, enum gate regression, 2.0 view refusal, parse-error side effects, trailing-comment parse errors). Nothing exploitable found.
  • Kept as documented residuals:
    • READ_ONLY is no write barrier against postgres_execute in an explicit transaction. Only engine SQL runs on that connection; the docstring and the example README say so.
    • Stream names match without regard to case.
    • Three rare query_relation shapes (CREATE SECRET, EXECUTE of a prepared count(*) AS "Count", a PIVOT whose only column is Count).

Follow-ups (not in this PR)

  • Sandbox temp_directory / .tmp widening: a separate security PR, as decided.
  • DuckDB 2.0 type and time-zone drift: cursor time zone, HUGEINT/TIMESTAMPTZ gate drift, discover emitter types. A follow-up PR before 0.22.0, as decided.
  • dbt-duckdb / dlt files: they follow the DuckDB that writes them (documented in the upgrade notes). A follow-up PR, as decided.
  • Postgres/MySQL discovery and forge_db_tools: the ATTACH fails under the sandbox (Unknown secret storage found: 'local_file'), so fluid init --discover postgres:// is broken on every version. It needs the runner's temporary-secret fix.
  • Live round trips in CI: no CI job runs the live Postgres/MySQL round trips yet (they need service containers).

Documentation

fas89 added 13 commits October 21, 2026 22:54
…test.py leaks

tests/cli/conftest.py sets FLUID_LOG_LEVEL=DEBUG, and some xdist workers keep
it. The CLI subprocesses here inherited it, so the loader's DEBUG copy of a
warning the command reports was printed too. The tests that count the warning
once then failed with `assert 2 == 1`, as #734's TestValidateStrict did on
main's CI run for 1320619. The subprocesses now drop FLUID_LOG_LEVEL,
FLUID_DEBUG and FLUID_SHOW_REGISTRATION, as tests/output_ports already do,
with API keys as before.

Same change as #735's 57abff3, so #735 rebases onto it cleanly.
DuckDB 2.0.0 (2026-10-21) turned main red: 51 tests across Test 3.11-3.14,
Coverage and the DuckDB e2e legs. Python 3.10 stayed green because DuckDB 2.0
needs 3.11, so pip resolves 1.5.6 there. The floor stays duckdb>=1.5.0 and no
ceiling is added; one small seam, providers/_duckdb_compat.py (the shape of
fluid_build/_mcp_compat.py), holds what differs.

- Relations have no fetchone() in 2.0, and a con.execute() result streams and
  owns its connection until read to the end: after fetchone() on a one-row
  COUNT(*), the next relation read, register or create_function raises
  ResourceInUseException. first_row() reads to the end on both versions; all
  18 DuckDB fetchone sites use it (verify, local provider, contract_tests,
  duckdb runner, late-arrival split, embedded-SQL parse, local validation,
  diff live, verify masking). An AST fence in tests/providers/test_duckdb_compat.py
  refuses fetchone/fetchmany/fetch_df_chunk/fetch_arrow_table/fetch_record_batch
  anywhere in fluid_build except modules of other database clients.
- A database file 2.0 creates uses storage version 69, which 1.5.6 cannot open.
  secure_duckdb_connect now passes storage_compatibility_version='v1.5.0' at
  connect on 2.x only (a SET after open is too late for the main file), so the
  files forge-cli creates stay readable by every supported DuckDB; a file that
  exists keeps its format, a caller's own setting wins, and the setting is
  locked with the rest. 1.5.x behaviour is unchanged.
- 2.0's sqlite scanner goes through DuckDB's file system, so the sandbox
  allowlist now bounds it (outside file, ./.., symlink and ATTACH refused). The
  pin that documents 1.5's unbounded scanner runs on 1.5 only, and a new one
  pins 2.0's bound. The engine still keeps contract SQL off any connection
  that loads a scanner (postgres stays unbounded).

Sources: duckdb-python v2.0.0 release notes ("Upgrading from 1.x");
https://duckdb.org/docs/2.0/clients/python/migration.html;
https://duckdb.org/docs/2.0/operations_manual/migration.html;
narwhals-dev/narwhals#4078 (the fetchall pattern).
On DuckDB 2.0, con.sql() of SQL whose last statement returns no rows
(CREATE TABLE ... AS, an INSERT without RETURNING) returns a relation over
the row count where 1.5 returned None. A local SQL action or contract_tests
action with such SQL then wrote a one-row "Count" file as its output and
reported ok; on 1.5 it failed on 'NoneType' object has no attribute
'write_csv'. query_relation() in providers/_duckdb_compat.py restores 1.5's
answer on 2.x (by the last statement's type, then by the Count/Success
shape for DML), and both call sites now fail with a message saying to end
the SQL with a SELECT. Found by two read-only audits of fluid_build against
the 2.0 change list.

Also from those audits:
- providers/local/mocks.py: DuckDB 2.0 refuses CREATE SCHEMA
  information_schema (a system catalog); on 1.5 it was a no-op. Dropped.
- cli/verify.py closes its DuckDB connection when reading the file fails.
- confine_declared's docstring no longer says the sqlite scanner is
  unbounded on every version.
- New tests pin what the fetchone fix restored but nothing asserted: a local
  SQL action's row count and a materialize step that stays "materialize"
  (on 2.0 it fell back to a skipped noop after writing its file).
- tests/test_contract_tests_branches.py's mock pinned the 1.x fetchone call.
A verification sweep ran the port against duckdb 1.5.6 and 2.0.0, on
Python 3.10, 3.12 and 3.14: sandbox attack probes, about 900 live fluid CLI
runs on the 13 quickstart templates and the local examples, a CI-job replay,
mutation testing, a drift diff and concurrency probes. Its findings that
the port or DuckDB 2.0 caused, fixed:

- is_unloaded_function: DuckDB 2.0 no longer names postgres_scan's
  extension ("Table Function with name postgres_scan does not exist!"), so
  contract SQL calling it got a bare catalog error instead of the sandbox
  hint; mysql_scan never had one. Both read as a refusal now. Access was
  never affected.
- MCP DuckDBDriver.health_check ran on the shared connection; on 2.0 a
  second thread there refuses ("connection is busy") instead of waiting.
  It takes a cursor, as execute() already does. A concurrency test pins
  both.
- The partial-read fence also catches rel.arrow(), fetch_arrow_reader(),
  record_batch() and a relation's removed execute(), and fails when it
  scans nothing (a mutant pointing it at an empty tree survived).
- Tests for what mutants showed unpinned: first_row returns the first of
  several rows; contract_tests' dry run counts a real relation; the
  duckdb runner's remote-landing probe leaves its connection usable.
- CHANGELOG: `fluid demo` (not `fluid init`) creates .fluid/db.duckdb; the
  pinned format opens on DuckDB 1.5.0 and later; dbt-duckdb and dlt files
  follow the DuckDB that writes them; contract SQL changed in 2.0 (the
  `->` lambda, integer // and % by zero).
Seen in a live `fluid apply --mode amend-and-build` of a quickstart whose
build SQL was a CREATE TABLE ... AS: under the build's heading the error said
"SQL action 0 ... also set output_table". A contract author writes neither
actions nor output_table. Both call sites now print one message from
_duckdb_compat.NO_ROWS_TO_WRITE that ends with a working example
(CREATE TABLE t AS SELECT ...; SELECT * FROM t), and a test runs that
example through local apply on both DuckDB versions.
pyproject.toml takes duckdb>=1.5.0 with no ceiling, and DuckDB 2.0 needs
Python 3.11, so 1.5.x ran only on the Python 3.10 test leg, which resolves
the newest 1.5 patch (1.5.6), never the declared floor; and Python 3.10
reached end of life in October 2026. The job installs exactly the floor the
sandbox enforces (MIN_DUCKDB_VERSION, read from the code, so the two cannot
drift) on Python 3.12, asserts it is the version that runs, and runs the
compat seam, sandbox, MCP DuckDB driver and local end-to-end tests.
…uckDB 2.0

DuckDB 2.0's postgres extension dropped postgres_scan(dsn, schema, table), so
a DuckDB acquisition build reading Postgres failed on every Python 3.11+
install ("Table Function with name postgres_scan does not exist!"); main
fails the same. Found by the documentation dimension of the world-class
inspection, then reproduced live against a local Postgres 16 container.

The runner now attaches the source before the sandbox's lock, as it attaches
MySQL and SQLite sources, and reads <alias>.<schema>.<table>:

- ATTACH '<libpq dsn>' AS postgres_<build> (TYPE postgres, SECRET
  postgres_<build>, READ_ONLY). READ_ONLY: postgres_execute and INSERT on the
  alias are refused (probed on 1.5.6 and 2.0.0), so nothing on the
  connection can write to the source.
- An empty CREATE TEMPORARY SECRET named in the ATTACH: without one, the
  extension looks for a default secret in every storage, and the persistent
  storage the sandbox turns off answers "Unknown secret storage found:
  'local_file'" on 1.5.0, 1.5.6 and 2.0.0 alike. The DSN carries the
  credentials and stays authoritative (a wrong password is refused).
- connection.secretRef is resolved into the password, as the MySQL branch
  already did; the shared DSN builder still refuses control characters and a
  non-numeric port.
- A connect error is raised as _SourceAttachError with the password masked
  (one helper for the postgres and mysql attaches), and _execute turns it into
  a FAILED run with a run record, so `fluid apply` reports "duckdb build ...
  failed: source: ..." instead of "Unexpected error during execution".

Live proof, examples/source-aligned-postgres-duckdb through the fluid CLI
against postgres:16-alpine: validate, apply --mode amend-and-build (5 rows)
and verify --strict pass on duckdb 2.0.0, 1.5.6 and 1.5.0; on main 2.0.0 the
apply fails as above. TestDuckdbPostgresLive passes on 3.10/1.5.6,
3.12/1.5.0, 3.12/1.5.6, 3.12/2.0.0 and 3.14/2.0.0. A wrong password fails the
build with the password masked in the output, logs and run record. A new
offline test runs the runner against an unreachable server and asserts a
connection failure (it fails on 2.0 with the old postgres_scan read).

The example's README now uses `fluid apply --mode amend-and-build
--build-id`, which replaced `--build`.

Reference: https://duckdb.org/docs/2.0/core_extensions/postgres/overview.html
(ATTACH, postgres_query and postgres_execute; no postgres_scan).
- Strict mypy (CI's typecheck-strict job) failed on cli/_verify_masking.py:
  first_row() is Optional. New one_row() raises on an empty result and is
  used where an aggregate's row is indexed straight away (verify, verify
  masking, local provider, contract_tests, late-arrival counts).
- query_relation() still let two DuckDB 2.0 shapes through as results: a
  CREATE TABLE ... AS PIVOT (typed MULTI, it returns a Success flag) and
  SQL ending in DML after a query (2.0's con.sql() returns the earlier
  query's rows). On 2.x every statement but the last now runs first and the
  last one alone decides; MULTI is judged by its shape. On 1.5 it is con.sql
  itself, the reference. Parametrized cases for DML, ALTER, SET, DROP and
  BEGIN...COMMIT endings, and the local apply refuses all three shapes.
- The no-rows message said "CREATE TABLE t AS ...; SELECT * FROM t", which
  fails silently when the planner also sets output_table (its CTAS wrap
  cannot hold several statements); it now says to write just the SELECT.
- The storage pin is v1.0.0, the format DuckDB 1.5 itself writes (storage
  version 64, which DuckDB 1.0 and later open), not v1.5.0 (version 68,
  which 1.4 refuses and which accepts 2.0-only types such as VARIANT that 1.5
  refuses). A byte-level test pins header version 64 on both DuckDB versions.
- Reverted the is_unloaded_function broadening of 9c29f7d: postgres_scan is
  gone from DuckDB 2.0's extension, not unadvertised, and mysql_scan never
  existed, so "does not exist" there is not a refusal. A 2.0 test pins that
  postgres_scan is absent even with the extension loaded.
- The sandbox's scanner bullet is version-qualified with what was replayed
  on 1.5.0, 1.5.6 and 2.0.0: through 1.5 postgres_scan reaches any host and
  the sqlite scanner any file; 2.0 has no postgres_scan and bounds sqlite; on
  both an ATTACH of postgres or mysql after the lock is refused, so
  postgres_query / mysql_query reach only what the engine attached. The
  postgres pin runs on 1.5 only, and a new pin covers the refused ATTACH.
- The partial-read fence also flags an execute() of a query whose result is
  thrown away (local_validation's SELECT 1 probe now reads it), and fails if
  an exempt module starts using DuckDB. The MagicMock dry-run test that
  mirrored first_row's internals is gone; a real-relation test covers it.
- contract_tests' docstring no longer says CREATE TABLE AS SQL is accepted.
- CHANGELOG: the legacy module is named fluid_build.contract_tests; the
  Postgres fix; the conversion recipe (COPY FROM DATABASE on 2.x, verified
  readable by 1.5.6); files forge-cli 0.21.0 wrote on DuckDB 2.0.
A mutation pass on the round-2 code showed _NO_ROWS_STATEMENTS redundant:
once the last statement runs alone, every DDL, SET, ATTACH, LOAD and
transaction statement returns a Count or Success shape on DuckDB 2.0 (probed
for 27 statement kinds), so the set never changed an answer. It is gone; a
query type (SELECT, EXPLAIN, CALL, PRAGMA) keeps its relation, as DuckDB 1.5
does even for CHECKPOINT's Success flag (now a test case), and anything else
is judged by its shape.

The same pass showed no test of a postgres source that cannot connect and
has no password: the unreachable-source test now runs with and without one,
so an attach error that escapes _execute as a raw exception fails it.
…fail the run

Found by the defect scan's recon, then reproduced live against MySQL 8.4:
every DuckDB acquisition build reading MySQL or MariaDB failed before it
connected, on DuckDB 1.5.6 and 2.0.0 alike (main too), with "Unknown secret
storage found: 'local_file'". Without a secret named in the ATTACH, the mysql
extension looks for a default one in every secret storage, and the sandbox
turns the persistent one off; the postgres attach hit the same and got an
empty temporary secret in bd71a01. The mysql attach now gets one too
(_empty_secret_sql, shared), and is READ_ONLY like the postgres one: CREATE
and mysql_execute on the alias are refused (probed live).

A connection block the DSN builder refuses, or a secretRef that does not
resolve, raised a bare ValueError out of the connect, which `fluid apply`
printed as "Unexpected error during execution" with no run record. It is now
a _SourceSetupError (a _SourceAttachError and still a ValueError), so the run
fails with the reason; an extension that did not load raises
_SourceAttachError too. The messages name the field or the reference, never
a value.

TestDuckdbMysqlLive and TestDuckdbPostgresLive pass on 3.10/1.5.6,
3.12/1.5.0, 3.12/1.5.6, 3.12/2.0.0 and 3.14/2.0.0 (local mysql:8 and
postgres:16-alpine). The offline unreachable-source test now runs for postgres
and mysql, with and without a password, and asserts each extension's own
connection failure: against main's code it fails 4/4 on 2.0.0 and the 2
mysql cases on 1.5.6.
- A secretRef whose backend fails (vault, aws, gcp, azure or file raise
  ConfigurationError, not ValueError) escaped the runner, so `fluid apply`
  ended with no run record and skipped its later builds. Any error resolving
  the secretRef now fails the source setup like a bad port or an unset
  variable does. A new end-to-end test checks exit 1 and a failed run record
  for a bad port, an unset env:// variable and a file-backend refusal, on
  postgres and mysql; narrowing the catch back to ValueError fails 4 tests.
- _postgres_attach_sql's docstring said READ_ONLY refuses postgres_execute.
  It does not: inside an explicit transaction, postgres_execute('COMMIT')
  and then a CREATE TABLE wrote to a live Postgres 16 on DuckDB 1.5.6 and
  2.0.0. It now says READ_ONLY refuses writes through the attached catalog,
  and that only the engine's own SQL runs on that connection.
- The duckdb-floor job retries its installs as the other jobs do, and also
  runs the DuckDB runner's tests and every suite the DuckDB end-to-end job
  runs (quickstarts, examples, the local happy path). The new
  tests/test_ci_duckdb_floor.py fails when the end-to-end job runs a file
  the floor job does not, or when a test that reads the DuckDB version is
  missing from it. On duckdb 1.5.0 the list gives 455 passed; the 2 failures
  are the macOS-only /var tests that fail the same on main.
- VARIANT is not a 2.0-only type (DuckDB 1.5.0 has it): the CHANGELOG and
  the storage-pin comment now say a VARIANT column needs storage v1.5.0 and
  is refused in the pinned format on 1.5 and 2.x alike.
- CHANGELOG: only a MySQL or MariaDB source used to end `fluid apply` with
  "Unexpected error during execution"; a Postgres source already failed the
  build.
- Two sandbox test docstrings no longer say the sqlite scanner is never
  bounded and that postgres stays unbounded.
Postgres sources (regressions of bd71a01's ATTACH):
- An enum column: postgres_scan typed it VARCHAR, the ATTACH-ed catalog
  types it ENUM(...), so a contract declaring the column VARCHAR failed the
  schema gate (mood/type_changed->fail) on DuckDB 1.5.6 and 2.0.0 where
  0.21.0 passed. The Postgres SELECT now casts enum columns to VARCHAR (an
  enum array to VARCHAR[]), as the masking projection replaces columns.
  Live, the cross-file hunter's contract fails on 084f5b8 and passes here
  on both versions, landing mood as VARCHAR.
- The schema gate fingerprints on a connection of its own and swallows a
  failed fingerprint, so a fingerprint that could not attach the source
  would land drifted data; nothing pinned it. A sqlite source with a strict
  schema it drifts from now has a test (SchemaDriftError, nothing landed).
- CHANGELOG: an upgrade note that a Postgres secretRef is now resolved
  (0.21.0 ignored it and libpq's PGPASSWORD supplied the password), that an
  enum column still reads as text, and that stream names match without
  regard to case.

query_relation on DuckDB 2.x, against 1.5's answers:
- SQL whose later statement does not parse ran the statements ahead of it
  (2.0's con.sql() runs them before it raises); 1.5 runs none. It now
  raises from extract_statements before anything runs.
- A comment after the last ';' broke the relation 2.0 returns: aggregate,
  limit and write_csv re-read the statement's text and fail ("syntax error
  at or near ';'"). The trailing comments are cut, found with
  duckdb.tokenize (byte offsets, so a multibyte string is cut right).
- Statements ahead of the last are run with 2.0's complete() instead of
  fetchall(), so a large SELECT among them is not read into Python.

relation_view: 2.0 refuses a view over rows it already holds (a PIVOT
without an IN list, nextval(), RETURNING), which failed a local SQL action's
s3:// output and contract_tests' Parquet output. relation_view falls back to
a table; contract_tests' view, which only its unreachable COPY used, may
fail and its pandas fallback writes the file.

Also: detect-secrets flagged `secret == "CREATE TEMPORARY SECRET ..."` in two
tests and the shifted baseline line of test_security_regressions.py (CI's
Lint job would have failed); MySQL docstrings name the SECRET and READ_ONLY
attach; scripts/verify_engines.sh read a relation with fetchone(); the live
round trips' skip reason no longer says CI runs them; the example README
says READ_ONLY is no write barrier and suggests a read-only role; the new
CI guard test has the blank line the license writer expects.

Each fix has a test that fails without it (7 mutants, all killed).
…tion parses

The full suite on a2506ac found two test-side breaks of the third round:

- test_every_duckdb_connection_goes_through_the_sandbox flags every call on
  the duckdb module, and _duckdb_compat now calls duckdb.tokenize to find a
  statement's trailing comments. tokenize takes no connection and runs no
  SQL (a nanobind function of _duckdb, unlike the default-connection
  wrappers such as duckdb.execute, which document a connection parameter).
  The fence exempts that one name; a test pins that tokenize documents no
  connection while execute does, and that a near name is still a bypass.
- test_a_failed_sql_action_logs_its_error_redacted's fake connection had
  only sql(); on DuckDB 2.x query_relation parses with extract_statements
  first, so the fake raises its error there too.
@github-actions github-actions Bot added provider Changes or requests related to providers ci Continuous integration and automation changes security Security-related changes or reports cli Changes to the CLI surface or implementation tests Test coverage or test infrastructure changes needs-docs Pull request needs a linked docs update or justification labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown

📄 Documentation Reminder

This PR appears to be missing a documentation reference. Our docs live in a separate repo.

Please update the PR description with one of:

  • Link a docs PR — check the "Docs PR linked" box and paste the URL
  • Mark as no docs needed — check "No docs needed" with a justification
  • Acknowledge docs TODO — check "Docs TODO" and create the docs PR before merge

See the Contributing Guide for details.

Comment thread tests/providers/test_duckdb_compat.py Fixed
Comment thread tests/providers/test_duckdb_compat.py Fixed
CodeQL's py/implicit-string-concatenation-in-list flagged two parametrize
entries split over two lines (a MERGE and a CTAS-then-INSERT); a '+' makes
each one string on purpose.
@fas89
fas89 merged commit 7289a66 into main Oct 22, 2026
39 checks passed
@fas89
fas89 deleted the fix/duckdb-2-compat branch October 22, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration and automation changes cli Changes to the CLI surface or implementation provider Changes or requests related to providers security Security-related changes or reports tests Test coverage or test infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant