Register SQLite per-connection state at engine creation for the vector stores (fixes #1568) - #1569
Draft
edwinyyyu wants to merge 1 commit into
Draft
Conversation
…vector stores SQLiteVectorStore and SQLiteVecVectorStore registered their per-connection state -- PRAGMA foreign_keys=ON and the sqlite-vec extension loader -- through a connect listener in __init__, on the engine supplied through their params. A connect listener fires only for connections opened after it is registered, so any connection the engine pooled earlier served the store with foreign keys off (cascade deletes silently orphaning pending-operation rows) or without the extension (every vec statement failing with "no such module: vec0"). As MemMachine#1568 documents, registering listeners on an engine already serving traffic also races SQLAlchemy's event dispatch. Mirror the convention MemMachine#1545 established for the segment store: the DatabaseManager registers the state on the engines it creates, before anything is pooled -- enable_sqlite_foreign_keys, shared verbatim with MemMachine#1545 so merge order does not matter, and the new load_sqlite_vec_extension, which lives with the store that needs it -- and each store's startup() verifies instead of mutates, refusing an engine without the state with a directive naming the helper. The listener on the sync engine SQLiteVectorStore creates itself stays: that engine cannot have pooled anything before it. Test engines register the helpers at creation. Both faults reproduced on main by pooling one connection (SELECT 1) before constructing the store: the vector store reported 1 pending row before delete_collection and 1 orphaned after it with PRAGMA foreign_keys=0, and the vec store failed create_collection with "no such module: vec0". With creation-time registration the same script cascades (0 orphans) and creates the collection; without it each store now refuses at startup. Fixes MemMachine#1568 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JdJHiDWgrwNEYfARF8Yvic
edwinyyyu
marked this pull request as draft
September 1, 2026 23:40
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 2, 2026
Round-5 findings 1, 2, 4-8 (finding 3 filed as MemMachine#1571; finding 9 declined: the codec load's in-loop placement was chosen in round 2 so the open-existing path never materializes a codec it discards, and a load-once flag would add state to avoid a bare constructor on the rare retry). - get_segment_contexts took one snapshot per statement, so a deletion committing between the seeds statement and the context statements returned seeds with silently empty context. Windowed reads now issue one registry read after their last context statement, turning that window into the contractual stale-handle error. Pinned on both dialects by staging the deletion inside the context query; verified to fail without the check. Seeds-only reads are one statement and need nothing. - The IntegrityError fallback warned about an incarnation collision for a case it cannot distinguish from a key conflict whose winner was deleted concurrently; the warning now names both readings. - get_segment_store after close() raises ResourceManagerClosedError, a MemMachineError, so the condition is catchable by type and its test no longer pins a message substring. - The partition-key regex drops the anchors fullmatch makes redundant -- `$` matching before a trailing newline was the round-2 bug -- and the validator docstring names PARTITION_KEY_MAX_BYTES instead of hardcoding 32. - The properties-json In leaf casts each member once and takes the column's cast from the first pair. - The test engine fixture imports enable_sqlite_foreign_keys at module level. The helper stays in database_manager: PR MemMachine#1569 (fixing MemMachine#1568 for the vector stores) carries a byte-identical copy there so the two merge in either order. - Datetime normalization at filter-node construction reaches the Neo4j compiler too, whose datetime.timestamp() read a naive value in the server's local zone; it now receives UTC-aware instants like every other backend. Recorded as a deliberate alignment in the design doc, the PR body, and a comment at the coercion site; the construction invariant is pinned by the filter-parser tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 2, 2026
Round-5 findings 1, 2, 4-8 (finding 3 filed as MemMachine#1571; finding 9 declined: the codec load's in-loop placement was chosen in round 2 so the open-existing path never materializes a codec it discards, and a load-once flag would add state to avoid a bare constructor on the rare retry). - get_segment_contexts took one snapshot per statement, so a deletion committing between the seeds statement and the context statements returned seeds with silently empty context. Windowed reads now issue one registry read after their last context statement, turning that window into the contractual stale-handle error. Pinned on both dialects by staging the deletion inside the context query; verified to fail without the check. Seeds-only reads are one statement and need nothing. - The IntegrityError fallback warned about an incarnation collision for a case it cannot distinguish from a key conflict whose winner was deleted concurrently; the warning now names both readings. - get_segment_store after close() raises ResourceManagerClosedError, a MemMachineError, so the condition is catchable by type and its test no longer pins a message substring. - The partition-key regex drops the anchors fullmatch makes redundant -- `$` matching before a trailing newline was the round-2 bug -- and the validator docstring names PARTITION_KEY_MAX_BYTES instead of hardcoding 32. - The properties-json In leaf casts each member once and takes the column's cast from the first pair. - The test engine fixture imports enable_sqlite_foreign_keys at module level. The helper stays in database_manager: PR MemMachine#1569 (fixing MemMachine#1568 for the vector stores) carries a byte-identical copy there so the two merge in either order. - Datetime normalization at filter-node construction reaches the Neo4j compiler too, whose datetime.timestamp() read a naive value in the server's local zone; it now receives UTC-aware instants like every other backend. Recorded as a deliberate alignment in the design doc, the PR body, and a comment at the coercion site; the construction invariant is pinned by the filter-parser tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
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.
Purpose of the change
Fix #1568.
SQLiteVectorStoreandSQLiteVecVectorStoreregistered SQLite per-connection state (PRAGMA foreign_keys=ON; the sqlite-vec extension loader) through aconnectlistener in their own__init__, on the engine supplied through their params. This is the structural fault #1545 fixed for the segment store, fixed here with the same convention.Description
The two faults, as #1568 lays them out:
connectlistener fires only for connections opened after it is registered. Anything the engine pooled earlier serves the store with foreign keys off -- a cascade delete silently orphans pending-operation rows, nothing errors -- or without the extension, so every vec statement fails withno such module: vec0.RuntimeError: deque mutated during iteration; the issue reports it reproduced in Overhaul segment store: shared tables with incarnation-scoped tenant keys (fixes #1544, #1546, #1549) #1545's concurrency tests).Both are latent on the production path today, since the
DatabaseManagercreates each engine immediately before constructing its store, but the params API accepts any engine.What changed:
database_manager.py:enable_sqlite_foreign_keys(engine)is added -- verbatim the helper from Overhaul segment store: shared tables with incarnation-scoped tenant keys (fixes #1544, #1546, #1549) #1545, at the same location -- and applied to theSQLiteVectorStoreengine at creation;load_sqlite_vec_extension(engine)is applied to theSQLiteVecVectorStoreengine at creation. The shared relational engine (async_get_sql_engine) is left to Overhaul segment store: shared tables with incarnation-scoped tenant keys (fixes #1544, #1546, #1549) #1545.sqlite_vector_store.py: the listener on the caller-supplied async engine is dropped. The listener on the sync engine the store creates itself two lines above stays, since nothing can have been pooled on it yet.startup()checksPRAGMA foreign_keysand refuses an unenforced engine with a directive naming the helper.sqlite_vec_vector_store.py: the loader moves out of__init__into the module-levelload_sqlite_vec_extension, next to the store that needs it.startup()probespragma_function_listforvec_versionand refuses with a directive. The probe keeps the check a scalar; callingvec_version()would need its failure told apart from otherOperationalErrors.sqlalchemy_sqlite_enginefixture does). Per store: a refusal test and a pooled-before-construction test. The manager tests assert each helper is applied to the engine before the store is constructed.Testing
Fail-before on untouched
main. Each script pools one connection (SELECT 1) before constructing the store, then exercises it:The same scripts on this branch, registering at engine creation:
Without registration each store now refuses at
startup():uv run --frozen --all-extras pytest packages/server/server_tests(default lane, integration excluded): 1873 passed, 3 skipped, 0 failedruff checkandruff format --check: cleanty check --project packages/server: 17 diagnostics, the same 17 asmain(pre-existing nebulagraph and OpenAI stub imports; none on touched lines)Compatibility
startup()now fails loudly with the directive otherwise. In-tree construction goes through theDatabaseManager, which does this. No config or schema change.enable_sqlite_foreign_keysis byte-identical in both PRs at the same location, andgit merge-treeof the two branches is conflict-free, so they can merge in either order.Type of change
startup(); only out-of-tree callers constructing stores on their own engines are affected)Fixes/Closes
Fixes #1568
🤖 Generated with Claude Code
https://claude.ai/code/session_01JdJHiDWgrwNEYfARF8Yvic