Skip to content

fix(duckdb): a remote discover could not read the host's CA bundle on 2.0.0 - #759

Merged
fas89 merged 7 commits into
mainfrom
fix/duckdb-ca-bundle
Oct 28, 2026
Merged

fas89 merged 7 commits into
mainfrom
fix/duckdb-ca-bundle

Conversation

@fas89

@fas89 fas89 commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

fluid init --discover s3://… and --discover https://… failed on duckdb 2.0.0 before a byte was read:

Permission Error: Cannot read a CA certificate bundle: file system access is restricted by
configuration. Add one of /etc/ssl/certs/ca-certificates.crt, … to allowed_paths, point
ca_cert_file at an allowed file, or disable server certificate verification

TLS needs the host's trust store, the sandbox granted no path to it, and nothing granted one when a remote prefix was declared — so the whole remote-discovery surface was broken on the engine the project resolves to by default, while the same call worked on 1.5.x. The cause is upstream and deliberate: duckdb/duckdb-httpfs#474 moved the bundle read inside DuckDB's own filesystem (CURLOPT_CAINFO_BLOB), where curl and OpenSSL used to open it themselves and bypassed allowed_paths entirely.

Trello: https://trello.com/c/C2fT4zj1 (P0, founder-confirmed 2026-10-27).

What it does

_confine_connection grants a private copy of the first CA bundle that exists out of DuckDB's five candidates — one file in allowed_paths, mode 0400 in a 0700 mkdtemp directory, ca_cert_file pinned to it with SET GLOBAL, removed when the connection object is finalised — and only when:

  • the allowlist carries a remote prefix (a local read gains nothing), and
  • the pin lands (no httpfs ⇒ DuckDB can never open the copy ⇒ no grant), and
  • the copy would not land inside a granted directory (see below).

allowed_directories is untouched, so what a remote prefix grants locally is unchanged (#757). The allowlist object is not widened — the grant is applied at SET time — while sandbox_refusal_hint does disclose that a copy of the trust store is readable, because saying otherwise would be false.

Why a copy, and why those three gates

Each of these was a defect in an earlier version of this PR, found by review and reproduced before fixing:

  1. A path in allowed_paths is granted for WRITING. The first version granted the host store directly; COPY (SELECT 'ATTACKER CA') TO '<store>' replaced a 333 KB trust store with 14 bytes on 1.5.6 and 2.0.0. Only file ownership stood in the way — /etc/ssl/cert.pem is root-owned — but fluid generate ci emits a python:3.12-slim job, which runs as root, and that job runs fluid apply; providers/local/local.py turns a contract's s3:// spec into a remote prefix and then runs that contract's own properties.sql on the connection. A contract could have installed its own CA into the trust store of the container it was built in.
  2. The 0400 is not a write bar. mkdtemp honours $TMPDIR, and a workspace-local $TMPDIR is an ordinary CI/direnv/Nix arrangement. There the copy's directory is writable by the SQL, and COPY TO with its default options writes a temp file and renames over the target — 333483 bytes → 14, measured on both engines. Hence _ca_dir_within_a_grant, and the grant is dropped with a logged warning. What holds is the reach: the host store is never granted, so this grant writes nothing outside the process's own temp directory.
  3. ca_cert_file must be pinned GLOBAL. A plain SET lands in LOCAL scope, so con.cursor() read '' while allowed_paths (GLOBAL) still showed it the copy: every cursor-based remote read failed on 2.0.0 with the original refusal, and the locked configuration meant no caller could fix it.

Certificate verification is untouched and stays on. An expired and a self-signed certificate are refused on 1.5.6 and 2.0.0 alike, with and without enable_server_cert_verification — that setting governs the httplib client while s3:///https:// go through curl. 1.5.6 exposes enable_curl_server_cert_verification (default true) and 2.0.0 no longer does; duckdb-httpfs#378 proposes unifying them and is still open, so the above is measured rather than quoted. Nothing here disables verification.

Reuse

Checked locally first: nothing in fluid_build/ handled a CA bundle (no cert.pem, certifi, SSL_CERT_FILE, ca_cert_file, get_default_verify_paths), and the sandbox already had the single chokepoint this belongs at.

Strategy: adapt the pattern, diverge on the list. The candidates mirror DuckDB's own five, in DuckDB's order, not truststore's and not certifi.where(): what gets copied has to be a store DuckDB itself would have trusted, and DuckDB consults neither OpenSSL's defaults nor SSL_CERT_FILE (measured — pointing all three at a granted copy still produced the five-candidate refusal). certifi is installed (via requests/httpx), but its bundle lives in site-packages, where overwriting it would reach every other tool in the environment.

Validation

Live, through the CLI, on 1.5.1, 1.5.6 and 2.0.0: --discover s3://noaa-ghcn-pds/csv/by_year/2024.csv → 8 columns, --discover …/duckdb/data/csv/customer.csv → 18, and fluid validate accepts the emitted contract. gs://, http://, https:// and s3:// each read with exactly one granted path. A clean suite run leaves no directory in $TMPDIR.

20 tests, all offline except the ones whose subject is the grant (they load httpfs and skip if it cannot be installed). Eleven mutations were reverted and the failures recorded in the test docstring, not predicted — including one count that dropped, and two tests that passed for the wrong reason until they were fixed. The file is registered in the duckdb-floor lane, which test_ci_duckdb_floor requires of anything branching on the engine version; that gate caught the omission.

Full suite at fdfbe575 under xdist, as CI runs it: 28048 passed, 4 failed — test_broad_roots_are_named, test_a_tmpdir_inside_a_project_does_not_make_the_project_a_temp_root, test_dbt_does_not_reload_the_project_dotenv, test_claude_code_live_keyless_envelope_roundtrip, all four pre-existing and all four reproduced on clean 5f2ec50b.

Review verdicts

Three rounds, every confirmed finding fixed, each reproduced before acting:

round raised / confirmed outcome
security + world-class inspection + defect scan — / 1 HIGH, 2 introduced MEDIUM-LOW the write-grant (HIGH) and two of mine
five-lens adversarial review 24 / 12 verified same HIGH independently; 4 survived and were fixed
round two (the redesign) 28 / 12 verified the 0400 write bar, the pin gate, the pin scope, the cleanup
round three (the gates) 23 / 2 both mine, both tests: flaky under pytest -n auto, one red-instead-of-skip offline

Known limitations (also in the CHANGELOG)

  • No operator override for a CA bundle outside DuckDB's five paths, so a host behind a TLS-inspecting proxy whose CA lives elsewhere still gets DuckDB's refusal. No worse than before — every such read failed on 2.0.0 regardless.
  • azure:// / az:// is unmeasured: the azure extension does its own TLS, and on that path the pin cannot land, so no copy is made.
  • Pre-existing and out of scope, found while measuring: is_remote_location returns False for any remote URL containing ? or # (_REMOTE_RE's path group is /[^?#]*), so presigned URLs and ? globs never load httpfs — and the redactor masks everything after ?, so the error reads 202?***REDACTED***. Also abfs:// is remote to the duckdb runner but not to the sandbox; confinement refuses it by realpath, so it is a functional gap, not an escape.

fas89 added 6 commits October 28, 2026 00:54
… 2.0.0

`fluid init --discover s3://…` and `--discover https://…` failed on duckdb
2.0.0 before a byte was read:

    PermissionException: Cannot read a CA certificate bundle: file system
    access is restricted by configuration. Add one of
    /etc/ssl/certs/ca-certificates.crt, … to allowed_paths, point
    ca_cert_file at an allowed file, or disable server certificate
    verification

TLS needs the host's trust store, the sandbox granted no path to it, and
nothing granted one when a remote prefix was declared — so the whole
remote-discovery surface was broken on the engine the project resolves to by
default, while the same call worked on 1.5.x. The cause is upstream and
deliberate: duckdb/duckdb-httpfs#474 moved the bundle read inside DuckDB's own
file system (`CURLOPT_CAINFO_BLOB`, an in-memory store for httplib), where
curl and OpenSSL used to open it themselves and bypassed `allowed_paths`
entirely.

`_confine_connection` now appends one CA bundle to `allowed_paths` — the first
of the five DuckDB probes that exists — and only when `allow.remote_prefixes`
is non-empty. One file, never a directory, so `allowed_directories` is
untouched and what a remote prefix grants locally is unchanged (#757). The
grant is applied at SET time and not folded into `allow`, so refusal hints and
the read-back still speak only of what the call site declared.

The candidate list mirrors DuckDB's own, in DuckDB's order, rather than
borrowing `ssl.get_default_verify_paths()` or `certifi` the way
sethmlarson/truststore picks a store for OpenSSL: the grant has to name the
file DuckDB will open, and DuckDB consults neither OpenSSL's defaults nor
`SSL_CERT_FILE`. certifi would also add a dependency the base install does not
ship. bergaaberg/dovre-packs#62 grants the system bundle into `allowed_paths`
the same way.

Measured, not assumed:

* Certificate verification is untouched and stays on. An expired and a
  self-signed certificate are refused on 1.5.6 and 2.0.0 alike, with and
  without `enable_server_cert_verification` — that setting governs the httplib
  client while s3 and https go through curl, whose own flag defaults to on
  (duckdb/duckdb-httpfs#378 unified the two). Nothing here disables
  verification or pins `ca_cert_file`.
* Plain `http://` never needed the grant: duckdb/duckdb-httpfs#499 made a
  failed bundle load non-fatal there, and 2.0.0 carries it.
* Live, through the CLI: `--discover s3://noaa-ghcn-pds/csv/by_year/2024.csv`
  emits 8 columns and `--discover …/customer.csv` emits 18, on 1.5.1, 1.5.6
  and 2.0.0; `fluid validate` accepts the result.

The 9 new tests are offline. Each mechanism was reverted and the failures
recorded: returning `None` fails 3, returning the bundle's directory fails 4,
dropping the remote-prefix guard fails 1. On 2.0.0 the refusal arrives before
any connection is attempted, so one test parses DuckDB's own candidate list
out of its refusal and fails if upstream reorders it. The file is registered in
the `duckdb-floor` CI lane, which `test_ci_duckdb_floor` requires of any test
that branches on the engine version — it caught the omission.

Trello: https://trello.com/c/C2fT4zj1 (P0)
…test stops patching os

Two findings from the defect scan of the previous commit, both introduced by it.

The refusal hint told the user that contract SQL "may only read and write the
locations the contract declares and its own directory". With a remote prefix
declared that is now false: the trust store is readable too, which the commit's
own `test_duckdb_can_read_the_bundle_it_was_granted` proves. The hint now names
the bundle when one is granted, and says nothing about it otherwise, where it
would be noise on every local refusal. The allowlist object is still left
untouched — it is what every other caller reads as the declared-inputs list.

`test_the_granted_bundle_is_the_first_candidate_that_exists` and
`test_a_host_with_no_candidate_grants_nothing` patched `sandbox.os.path.isfile`,
and `sandbox.os` IS the stdlib module, so that replaced `posixpath.isfile` for
pytest and everything else running while a real connection was opened. They now
inject `_CA_BUNDLE_CANDIDATES` with real files under tmp_path, which exercises
the production probe instead of stubbing it out, and pins that an absent
candidate ahead of an existing one is skipped. A new test pins the same for a
directory and a dangling symlink at a candidate path — `os.path.isfile` answers
False for both, and the fix depends on that.

Also corrected the module docstring's http clause: plain `http://` did not NEED
the grant (duckdb/duckdb-httpfs#499 made a failed load non-fatal there) but it
gets one anyway, because the declared prefix is what the grant keys on and an
http base URL that redirects to https still verifies.

11 tests on 2.0.0, 10 + 1 skip on 1.5.6 and 1.5.1; the five sandbox suites pass
together (174) but for the two pre-existing workspace-root failures.
…st trust store

The previous commit granted the host's CA bundle to `allowed_paths` directly.
**A path in `allowed_paths` is granted for writing**, which the world-class
inspection of that commit found and I then reproduced on 1.5.6 and 2.0.0:

    COPY (SELECT 'ATTACKER CA' AS c) TO '<store>' (FORMAT CSV, USE_TMP_FILE false)

replaced a 333 KB trust store with 14 bytes, while an ungranted sibling in the
same directory stayed refused — so the grant is file-scoped, and that one file
was the trust store. Only file ownership stood in the way: `/etc/ssl/cert.pem`
is root-owned, so an unprivileged `fluid` is stopped by the OS. But
`cli/scaffold_ci.py` generates a `python:3.12-slim` job, which runs as root, and
that job runs `fluid apply --mode amend-and-build`; `providers/local/local.py`
turns a contract's `s3://` spec into a remote prefix and then runs that
contract's own `properties.sql` on the connection. A contract could have
installed its own CA into the trust store of the container it was built in —
worse than the read failure the previous commit set out to fix, and the same
class as #757/#758.

`_private_ca_bundle` now copies the chosen store into a `tempfile.mkdtemp`
directory (0700, outside the contract's tree), chmods the copy 0400, grants the
COPY, and pins `ca_cert_file` to it so DuckDB opens that file instead of walking
its own candidates. The copy is removed by the same `weakref.finalize` teardown
the spill directory uses. Two barriers: the store is not in `allowed_paths` at
all, and an unprivileged write to the copy is refused by its mode. Root defeats
the second, which is why the first matters — and why the copy is per-connection
and thrown away, so the damage stops at one connection's own TLS trust, over
remote locations the contract chose anyway.

The pin is load-bearing, not decoration: with it removed, a live `s3://` read on
2.0.0 fails with the original `Cannot read a CA certificate bundle`, because the
store is no longer granted.

Also from the same review, all verified before acting:

* `sandbox_refusal_hint` now says a private read-only copy of the trust store is
  readable, without printing the host path — SQL is refused that file, so naming
  it would point the reader at the wrong one.
* duckdb-httpfs#378 is OPEN, not merged. The docstring and CHANGELOG said it
  "unified" the two verification settings. What is true is measured: 1.5.6
  exposes `enable_curl_server_cert_verification` (default true), 2.0.0 no longer
  does, and an expired and a self-signed certificate are refused on both
  regardless of `enable_server_cert_verification`.
* `certifi` IS installed in the base environment, via requests and httpx, so
  "would add a dependency" was wrong. The reason to mirror DuckDB's own
  candidates stands and is now stated correctly: certifi's bundle lives in
  site-packages, where overwriting it would reach every other tool in the
  environment.
* The drift-guard test installs httpfs, so it now skips where that is not
  possible, as its sibling in `test_duckdb_sandbox.py` does, and the file's
  "everything here is offline" claim is corrected.

14 tests on 2.0.0, 13 + 1 skip on 1.5.6 and 1.5.1. Seven mutations, each
reverted and the failures recorded in the test docstring rather than predicted —
two of them exposed tests that passed for the wrong reason, including the write
test itself, which is refused on a developer machine by file ownership whatever
the allowlist says and now asserts the store's absence from `allowed_paths`.
Live: `--discover s3://…` emits 8 columns and `--discover …/customer.csv` 18, on
1.5.1, 1.5.6 and 2.0.0, and `fluid validate` accepts the result. A CLI discover
leaves no directory behind in $TMPDIR.
The adversarial review of dd1aeaa raised 24 findings, 12 verified by skeptics.
Its HIGH — the CA grant is a write grant — is the same defect the world-class
inspection found and 42320db fixed, reached independently by a second route.
Four items survived against the current code:

* `os.path.isabs` on a POSIX candidate list. From Python 3.13 `ntpath.isabs`
  answers False for a path with one leading slash, so the test failed on
  Windows under 3.13+ about the platform rather than the list. Measured:
  `ntpath.isabs('/etc/ssl/cert.pem')` is True on 3.12, False on 3.14. Now
  `posixpath.isabs`. No CI lane runs this file on Windows today — the Test
  matrix is ubuntu-only and the windows DuckDB lane runs a different file list
  — so this was a contributor-facing break, not a red build.
* Nothing pinned the candidate list where it can be checked everywhere. The
  drift guard reads the list back out of DuckDB's own refusal, but it needs
  2.0.0 and httpfs, and the `duckdb-floor` lane — the lane this file was added
  to — installs MIN_DUCKDB_VERSION 1.5.0, where it skips. The review measured
  that narrowing the production tuple to one absent path left 1.5.6 fully
  green. `test_the_candidate_list_is_pinned_literally` has no host or engine
  dependency, so an edit to the list has to be made twice, deliberately.
* Nothing pinned the MIXED allowlist the changed line builds. Every other test
  declares either a remote prefix or a local file, never both, so a regression
  in `allow.paths + [copy]` was invisible from this file.
  `test_a_declared_file_and_the_ca_copy_both_reach_allowed_paths` covers it and
  fails when the caller's paths are dropped. It asserts MEMBERSHIP: DuckDB's
  read-back order is its own — three files declared `zzz, aaa, mmm` came back
  `mmm, aaa, zzz`, neither sorted nor as declared.
* The refusal was quoted with a prefix DuckDB never prints. `PermissionException`
  is the exception type; the message starts `Permission Error:`. Spliced, the
  recorded symptom matched nothing in a log or a grep. Corrected here and in
  the CHANGELOG.

Two of the review's MEDIUMs were already fixed in 42320db (the open #378
citation, the certifi claim) and its remaining operational findings are recorded
as known limitations rather than silently left: there is no operator override
for a CA bundle outside DuckDB's five paths, which leaves a TLS-inspecting-proxy
host with no exit, and `azure://` is not measured.

`gs://`, `http://`, `https://` and `s3://` all read through the fix on 2.0.0
with exactly one granted path each (2, 18, 18 and 8 columns), so the CHANGELOG's
coverage claim is measured rather than assumed.

16 tests on 2.0.0, 15 + 1 skip on 1.5.6 and 1.5.1.
…annot be trusted

Round two of the adversarial review, aimed at the redesign itself. 28 findings,
12 verified; four mechanisms and a pile of prose. Everything below I reproduced
before acting on it.

**The 0400 is not a write bar.** `tempfile.mkdtemp` honours `$TMPDIR`, and a
workspace-local `$TMPDIR` is an ordinary CI, direnv or Nix arrangement. Where it
points inside a directory the connection grants, the copy's directory is
writable by contract SQL, and `COPY TO` with its DEFAULT options writes a
temporary file and renames it over the target — measured on 1.5.6 and 2.0.0,
333483 bytes of trust anchors replaced by 14. The mode only ever refused a plain
open. `_ca_dir_within_a_grant` now detects that case and the grant is dropped
with a `duckdb_ca_bundle_grant_skipped` warning, because granting a trust store
the SQL can replace is worse than granting none.

**The pin now gates the grant.** `ca_cert_file` lives in httpfs, so on a
connection that loaded a different extension the `SET` fails and DuckDB never
opens the copy — `cli/discover/filesystem.py` loads `azure` for an `azure://`
URL, which is exactly that path. The old code granted the copy anyway: a
writable file in `allowed_paths` plus a 330 KB copy for a benefit that cannot
exist. No pin, no grant, and the copy is removed at once.

**The pin is `SET GLOBAL`.** `ca_cert_file` is a LOCAL-scope setting, so a
session-scoped pin left `con.cursor()` with `''` while `allowed_paths` (GLOBAL)
still showed it the copy: on 2.0.0 every remote read through a cursor failed with
the original `Cannot read a CA certificate bundle`, and the locked configuration
meant no caller could fix it. Measured: GLOBAL, the cursor inherits it, both read
8 columns, and SQL still cannot change it after the lock.

**A failed copy cleans up after itself.** `mkdtemp` runs before the caller
registers the `weakref.finalize`, so an ENOSPC, an I/O error, or the candidate
disappearing between the `isfile` probe and the copy leaked the directory for
the lifetime of `$TMPDIR`.

Prose the review caught, all of it load-bearing for the next reader:

* the module's numbered pipeline (step 4) still described version 1 — the thing
  the redesign exists to stop doing — and omitted `ca_cert_file`;
* the comment above `sandbox_refusal_hint`'s branch still said a remote read
  "grants the host's trust store", three lines above the new comment saying the
  opposite;
* `_private_ca_bundle` claimed the 0400 made a write "refused by the OS as well
  as by the allowlist". The allowlist does not refuse it — that is the premise of
  the function — and the same docstring claimed "no state outside the process is
  touched" about a write that lands in `$TMPDIR`;
* the CHANGELOG said the copy "removes the capability" in the one threat model
  (a root container) where the mode buys nothing. It now says what is removed:
  the reach. The host store is not in `allowed_paths`, so nothing outside this
  process's temporary directory can be written;
* "removed on teardown" overstated the finalizer: `con.close()` alone does not
  remove the copy, measured; it goes when the connection object does, at
  interpreter exit, or not at all on `os._exit` — the same limits the spill
  directory documents;
* "every test here but one is offline" was false once the grant tests needed
  httpfs.

20 tests on 2.0.0, 19 + 1 skip on 1.5.6 and 1.5.1. The recorded mutation table
is re-measured against all 20 and now has eleven rows; one row's count DROPPED
(dropping the remote-prefix guard fails one test, not two, because the pin gate
independently blocks a local connection) and the table says so, since a count
that drifts silently teaches a maintainer to distrust it. Live: `--discover`
emits 8 and 18 columns on 1.5.1, 1.5.6 and 2.0.0; a clean suite run leaves no
directory in $TMPDIR.
…ree claims are corrected

Round three of the review. 23 findings, 2 confirmed — both mine, both in the
tests, and no production defect. Fixed, with the measured prose errors it also
turned up.

**Both $TMPDIR-counting tests were flaky under the project's own CI command.**
`_ca_directories()` listed the SHARED system temp directory, and ci.yml runs the
unit suite with `pytest -n auto --dist loadscope`: any other worker opening a
sandboxed connection with a remote prefix creates and tears one of these down,
so the count moved in both directions. The review measured a failure about one
run in six under concurrency. A `_private_tmpdir` fixture now points `$TMPDIR`
at the test's own directory, so only the copies the test provokes exist there.
Verified: six consecutive `-n 4 --dist loadscope` runs, 20 passed each, against
six background processes churning CA directories in the shared `$TMPDIR`.

**The exposed-TMPDIR test was RED, not skipped, where httpfs cannot be
installed.** It called `secure_duckdb_connect(..., extensions=["httpfs"])`
directly instead of the guarded helper — on an offline runner the INSTALL raises
and the test fails, and this file is in the `duckdb-floor` lane, so a download
blip would have failed that leg. The helper is now `_with_httpfs` (it is for any
test whose subject is the grant, not only the ones expecting one) and every
httpfs test goes through it.

Prose, each measured before changing:

* "the host store is not in `allowed_paths`, so nothing outside this process's
  own temporary directory can be reached" — false as written, in the docstring
  and again in the CHANGELOG. The same `SET allowed_paths` grants everything the
  call site declared; the sentence is about the CA grant alone and now says so.
* The comment justifying `SET GLOBAL` said `ca_cert_file` is "a LOCAL-scope
  setting". `duckdb_settings()` reports it GLOBAL on 1.5.1, 1.5.6 and 2.0.0
  until a plain `SET` puts a local value there — the behaviour is real and the
  GLOBAL keyword is necessary, but what makes it necessary is SET's own scope
  resolution. A maintainer checking the scope column would have found the
  opposite of what the comment claimed.
* `sandbox_refusal_hint` still told the user "a private read-only copy", the
  claim this branch retracts everywhere else — the CHANGELOG and the module
  docstring both now say the mode is a speed bump, not a write bar. It was the
  one place the superseded wording survived, and the one place a user reads. A
  new assertion fails if it comes back.

The pin-failure branch was the only silent one; it now logs at DEBUG, not
WARNING, because for the azure and local paths dropping the grant is the correct
outcome and a warning would cry wolf on every run — but the day a DuckDB release
renames or refuses that setting, remote reads go back to failing with
'Cannot read a CA certificate bundle' and nothing else would say why.

20 tests on 2.0.0, 19 + 1 skip on 1.5.6 and 1.5.1; providers + output_ports
2552 passed with only the two pre-existing workspace-root failures.
@github-actions github-actions Bot added provider Changes or requests related to providers ci Continuous integration and automation changes tests Test coverage or test infrastructure changes needs-docs Pull request needs a linked docs update or justification labels Oct 28, 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.

CodeQL flagged both on #759: a helper that returns a value and then ends on
`pytest.skip(...)` reads as mixing explicit returns with an implicit `return
None`, because the analysis cannot see that `pytest.skip` raises.

`raise pytest.skip.Exception(...)` is the same class by the public alias, so the
behaviour is unchanged and the exceptional path is explicit. Proved rather than
assumed: with the candidate list forced empty and with the connect call made to
raise, both helpers report SKIPPED with their own messages, and the
`raise AssertionError` placed after each call never runs.

20 tests on 2.0.0, 19 + 1 skip on 1.5.6 and 1.5.1.
@fas89
fas89 merged commit 93be67a into main Oct 28, 2026
38 checks passed
@fas89
fas89 deleted the fix/duckdb-ca-bundle branch October 28, 2026 05:33
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 needs-docs Pull request needs a linked docs update or justification provider Changes or requests related to providers tests Test coverage or test infrastructure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant