Repository navigation
fix: sweep a stale persist cache on a connection that carries its extensions - #1011
Merged
Merged
Conversation
…able `prepare_persist` sweeps a stale cache before the connection has loaded any `[[dirsql.extension]]`, so `DROP TABLE` on a virtual table whose module came from an extension answers `no such module` and wedges the cache permanently. #956 covered this flow with FTS5, whose module is compiled into SQLite and so is registered on every connection — a built-in standing in for a loadable one. The fixture extension now registers a virtual table module (`dirsql_testext_vtab`) alongside its scalar function, which no built-in can substitute for, and `build_fixture_extension` moves to `tests/common/` so the integration and e2e binaries share the one implementation. Both new tests fail today with the issue's error: no such module: dirsql_testext_vtab Refs #1008 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K4n9Q2DYnepFrNUzk94rTF
`prepare_persist` dropped the old user tables the moment it opened the cache — before anything had loaded the configured `[[dirsql.extension]]` entries or registered the `[[dirsql.function]]` workers. Dropping a virtual table needs its module registered on that connection, and a module that arrives with an extension is registered nowhere else, so `DROP TABLE notes_vec` answered `no such module: vec0`. Editing the `ddl` of a `--persist` cache holding one left it unopenable on every later run until `.dirsql` was removed by hand. The sweep is now deferred: `prepare_persist` only flags it (`needs_sweep`), and `finish_build` performs it after the extensions and functions are on the connection and before the batch that re-creates the tables. That is the invariant the bug was missing — the drop sees exactly the module set the batch that created these tables saw. Nothing between the two phases reads the swept tables: `reconcile_scan` is pure over the already-read file index, and the `warm` catalog probe that decides whether to re-run a batch comes after. Deferring rather than loading the extensions twice is deliberate: it is one connection, so a second `load_extension` would re-enter the init function and `sqlite3_create_module` rejects a name it already holds. Fixes #1008 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K4n9Q2DYnepFrNUzk94rTF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1008
Problem
prepare_persistdropped the old user tables the moment it opened the cache — before anything had loaded the configured[[dirsql.extension]]entries. Dropping a virtual table needs its module registered on that connection, and a module that arrives with an extension is registered nowhere else, soDROP TABLE notes_vecansweredno such module: vec0. Editing theddlof a--persistcache holding one left it unopenable on every later run until.dirsqlwas removed by hand.#956 covered exactly this flow — but with FTS5, whose module is compiled into SQLite and so is registered on every connection whether or not dirsql configured anything. A built-in module stood in for a loadable one, and the asymmetry went unnoticed.
Change
The sweep is deferred, not duplicated.
prepare_persistnow only flags it (needs_sweep);finish_buildperforms it after the extensions and functions are on the connection and before the batch that re-creates the tables. That is the invariant the bug was missing: the drop sees exactly the module set the batch that created those tables saw. Registering the declared[[dirsql.function]]workers first comes free from the same placement, so a schema object referencing one is droppable too.Loading the extensions a second time in
prepare_persistwas the alternative and is wrong: it is one connection, carried fromprepare_resolvedintofinish_build, so a secondload_extensionre-enters the init function andsqlite3_create_modulerejects a name it already holds.Nothing between the two phases reads the swept tables:
reconcile_scanis pure over the already-read file index, and thewarmcatalog probe that decides whether to re-run a batch comes after the new sweep site.Tests
Pushed RED first (c8ba191), confirmed red in CI for the right reason (run 32441733664:
a_rebuild_sweeps_a_cache_holding_an_extension_backed_virtual_table ... FAILED—no such module: dirsql_testext_vtab), then implemented in 40ea3cf.tests/ddl_batch.rs::a_rebuild_sweeps_a_cache_holding_an_extension_backed_virtual_table, the sibling Persist rebuild wedges on a cache holding an extension-provided virtual table:no such module: vec0#1008 asks for: same shape as the FTS5 test, but the module comes from a loaded extension.tests/ddl_batch_e2e.rs::a_persisted_cache_holding_an_extension_virtual_table_survives_a_ddl_edit, the real CLI with--persistacross addledit, asserting the next run is clean too (the wedge was permanent, so one rebuild squeaking through would not prove it fixed).prepare_persist_cold_start_reports_rebuildnow assertsneeds_sweep;prepare_persist_warm_cache_asks_for_no_sweeppins the other side;an_edited_ddl_sweeps_the_cache_before_rebuildingcovers the drop from the build side (an un-dropped table reads as warm, so the edited batch never re-runs).Supporting changes: the
tests/fixtures/testextextension now registers a virtual table module (dirsql_testext_vtab) alongside its scalar function — the thing no built-in can substitute for — andbuild_fixture_extensionmoves totests/common/mod.rsso the integration and e2e binaries share one implementation.Manual verification
Reproduced the issue's shape with the fixture extension in place of
sqlite-vec(/tmp/dirsql-1008-manual,.dirsql.tomldeclaring[[dirsql.extension]]+ anotes_vecvtab,on-file = "python3 rows.py {path}"):Parity
No parity impact. The fix is entirely inside the shared Rust core with no public API change, so all three SDKs pick it up identically — no drift introduced, none restored.
PARITY.mdneeds no edit.E2E Verification
--testscovers the*_e2e.rssuites; 40 test binaries green)packages/python/e2e-attestations/cc-keen-knuth-f11gyq.jsonreceipt writtenpackages/ts/e2e-attestations/cc-keen-knuth-f11gyq.jsonreceipt writtencargo test --manifest-path packages/rust/Cargo.toml --features cli --testscargo test --features cli --lib -p dirsql(770 unit tests)cd packages/python && uvx testing-conventions e2e attest 'uv run python -m pytest tests/e2e/ -x -q'cd packages/ts && uvx testing-conventions e2e attest 'pnpm test:e2e'uvx testing-conventions unit mutation --language rust --base origin/main --config testing-conventions.toml packages/rust→no surviving mutants (1 mutant(s) tested)dirsql-checks preflight --gate colocated-test --gate unit-lint --gate integration-lintand--gate e2e-verify→ 0 failing pairscargo clippy --workspace --features cli -- -D warnings,cargo fmt --all -- --checkChangelog / Migrations
packages/rust/changelog.d/2026-08-21-sweep-loads-extensions.md