5.4 KiB
Engine Persistence (C1) — Design
Date: 2026-07-30 Status: Approved direction (user: "продължи"); implementation follows.
Problem
After a server restart, FTS indexes, HNSW vector indexes, and graph objects silently vanish while the underlying rows survive (LSM+WAL):
CREATE INDEX ... USING FTSbuilds an in-memoryInvertedIndexonly (ctx.ftsIndexes); the DDL is never persisted under_schema:. DML keeps updating it in memory (exec/dml.nim:81-89, 295-304), but nothing writes it down.- Same for HNSW (
ctx.vectorIndexes) and graphs (ctx.graphs; only the backing<name>_nodes/_edgestables are durable). restoreSchema(exec/schema.nim:167-231) restores tables, views, triggers, users, policies and rebuilds B-tree secondary indexes — but not these three engines. TheirCREATEDDL is not even stored.- User-visible effect: hybrid search and graph queries return silently
empty results after restart (guards in
exec/eval.nim:103,150,968-984), and new inserts no longer update the lost indexes. This is a correctness bug, not a performance gap.
Goal
FTS indexes, HNSW vector indexes, and graphs survive a restart: after reopening the database, the same queries return the same results, and new DML keeps the indexes up to date.
Non-goals (later phases): snapshot-based persistence for fast startup (C2),
columnar persistence (no ctx integration exists yet), Raft transport (C3).
Design
Follow the existing schema-durability pattern: DDL is serialized under
stable _schema:<kind>:<name> keys in the LSM store and replayed at startup
by restoreSchema.
1. Persist engine DDL
New key prefixes (mirroring SchemaTablePrefix etc. in exec/schema.nim:14-20):
_schema:ftsidx:<table>.<column>→ the originalCREATE INDEX ... USING FTSDDL_schema:vecidx:<table>.<column>→ the originalCREATE INDEX ... USING HNSW/VECTORDDL_schema:graphs:<name>→ the originalCREATE GRAPH ...DDL
Written by the corresponding CREATE execution paths in the dispatcher
(executeQueryImpl), deleted by DROP INDEX / DROP GRAPH, and cleaned up
on DROP TABLE for keys belonging to that table (same sweep the current
code does for _schema:tables: and secondary-index metadata — check and
extend that path).
2. Restore on startup
restoreSchema runs after tables are restored and B-tree indexes rebuilt.
New step, in this order:
- Scan
_schema:ftsidx:/_schema:vecidx:keys, parse each DDL, and re-execute it throughexecuteQuery(the same way table DDL is re-applied). The CREATE INDEX path already builds the in-memory index from a full table scan, so replay == rebuild, no new build logic. - Scan
_schema:graphs:keys and re-execute the CREATE GRAPH DDL. Caveat to resolve at implementation time: replaying CREATE GRAPH must not fail when the backing<name>_nodes/_edgestables already exist (they were restored as regular tables). If the CREATE GRAPH execution errors on existing tables, the restore path instead rebuilds the in-memoryGraphobject by scanning those tables (small new loader in the graph engine usage site — the engine has no table-scan loader today, only unused binary file save/load). Pick whichever is smaller and matches existing behavior; document the choice in the plan.
Failures during engine restore must not prevent startup: log a warning and
continue (a corrupt engine DDL must not take the database down — matches
the defensive style of restoreSchema).
3. Ordering constraints
- Table restore + row data available BEFORE engine replay (indexes build
from scans; graphs build from
_nodes/_edges). - Engine replay runs before the context is served to connections
(it is part of
newExecutionContext→restoreSchema).
4. What does NOT change
- In-memory update paths in
exec/dml.nim(insert/update/delete keeping indexes current) — untouched; they work once the indexes exist again. - LSM/WAL mechanics;
_schematable format; B-tree index rebuild. - Public API: none.
Testing
New suite in tests/test_schema_persist.nim (the existing persistence test
file), TDD — each test fails before the fix:
- FTS: create table, insert docs,
CREATE INDEX ... USING FTS, close DB, reopen,hybrid_search-style query / FTS MATCH query returns the doc (pre-fix: silently empty). Insert another doc after reopen and verify it is found too (index updates live again). - Vector: same flow with an HNSW index and a vector search query.
- Graph: create graph, add nodes/edges, close, reopen, graph query returns
the traversal (pre-fix:
"[]"). - DROP INDEX removes the
_schema:ftsidx:/:vecidx:key (no ghost rebuild after reopen). DROP TABLE removes engine keys for that table. - Existing persistence tests keep passing (regression).
Verification gate: full nimble test green (currently 650 [OK]).
Risks
- CREATE GRAPH replay semantics (see §2) — resolved during planning with a concrete read of the dispatcher's graph DDL path.
- Startup cost: full-scan rebuilds are O(table size) per index, same as today's B-tree rebuild. Acceptable for C1; C2 (snapshots) addresses it.
- Re-executing DDL through
executeQueryinsiderestoreSchema— must not deadlock onctx.sharedLock(DDL lock is taken byexecuteQuery;restoreSchemaruns during context construction, before serving — verify no lock is held at that point).