Skip to content

Register SQLite per-connection state at engine creation for the vector stores (fixes #1568) - #1569

Draft
edwinyyyu wants to merge 1 commit into
MemMachine:mainfrom
edwinyyyu:fix/sqlite-vector-store-connection-state
Draft

Register SQLite per-connection state at engine creation for the vector stores (fixes #1568)#1569
edwinyyyu wants to merge 1 commit into
MemMachine:mainfrom
edwinyyyu:fix/sqlite-vector-store-connection-state

Conversation

@edwinyyyu

Copy link
Copy Markdown
Contributor

Purpose of the change

Fix #1568. SQLiteVectorStore and SQLiteVecVectorStore registered SQLite per-connection state (PRAGMA foreign_keys=ON; the sqlite-vec extension loader) through a connect listener 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:

  1. A connect listener 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 with no such module: vec0.
  2. Registering listeners on an engine already serving traffic races SQLAlchemy's event dispatch (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 DatabaseManager creates each engine immediately before constructing its store, but the params API accepts any engine.

What changed:

Testing

Fail-before on untouched main. Each script pools one connection (SELECT 1) before constructing the store, then exercises it:

register_at_creation=False: pending rows before delete=1, orphaned after=1, PRAGMA foreign_keys=0
register_at_creation=False: create_collection FAILED: OperationalError: (sqlite3.OperationalError) no such module: vec0

The same scripts on this branch, registering at engine creation:

register_at_creation=True: pending rows before delete=1, orphaned after=0, PRAGMA foreign_keys=1
register_at_creation=True: create_collection OK

Without registration each store now refuses at startup():

RuntimeError: SQLite engine does not enforce foreign keys, so deleting a collection would silently orphan its pending-operation rows. Register PRAGMA foreign_keys=ON at engine creation (enable_sqlite_foreign_keys) before constructing the store.
RuntimeError: SQLite engine does not load the sqlite-vec extension, so every vec statement would fail. Register the loader at engine creation (load_sqlite_vec_extension) before constructing the store.
  • uv run --frozen --all-extras pytest packages/server/server_tests (default lane, integration excluded): 1873 passed, 3 skipped, 0 failed
  • ruff check and ruff format --check: clean
  • ty check --project packages/server: 17 diagnostics, the same 17 as main (pre-existing nebulagraph and OpenAI stub imports; none on touched lines)

Compatibility

  • Out-of-tree callers constructing either store on their own engine must register the matching helper at engine creation; startup() now fails loudly with the directive otherwise. In-tree construction goes through the DatabaseManager, which does this. No config or schema change.
  • Relation to Overhaul segment store: shared tables with incarnation-scoped tenant keys (fixes #1544, #1546, #1549) #1545: enable_sqlite_foreign_keys is byte-identical in both PRs at the same location, and git merge-tree of the two branches is conflict-free, so they can merge in either order.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (a store now refuses an engine without its per-connection state at 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

…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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite per-connection state (foreign keys, sqlite-vec extension) is registered per-store on caller-supplied engines

1 participant