feat(persist): standalone B-tree indexes survive restart
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

This commit is contained in:
2026-07-30 16:53:25 +03:00
parent fcb6237caf
commit 8d2d97ad94
3 changed files with 102 additions and 4 deletions
+1
View File
@@ -19,6 +19,7 @@ const
SchemaFtsIndexPrefix* = "_schema:ftsidx:"
SchemaVecIndexPrefix* = "_schema:vecidx:"
SchemaGraphsPrefix* = "_schema:graphs:"
SchemaBtreeIndexPrefix* = "_schema:btreeidx:"
## Legacy CREATE TABLE keys (pre-fix) used a migrations: counter suffix
SchemaLegacyCreatePrefix* = "_schema:migrations:"
+23 -4
View File
@@ -878,11 +878,12 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
dataKeys.add(key)
for key in dataKeys:
ctx.db.delete(key)
# Remove persisted FTS/HNSW index schema keys for this table
# Remove persisted FTS/HNSW/B-tree index schema keys for this table
var engineKeys: seq[string] = @[]
for (key, _) in ctx.db.scanAll():
if key.startsWith(SchemaFtsIndexPrefix & prefix) or
key.startsWith(SchemaVecIndexPrefix & prefix):
key.startsWith(SchemaVecIndexPrefix & prefix) or
key.startsWith(SchemaBtreeIndexPrefix & prefix):
engineKeys.add(key)
for key in engineKeys:
ctx.db.delete(key)
@@ -1387,6 +1388,15 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
if idxVal.len > 0 and not isNull(idxVal):
let lsmKey = if "$key" in row: stmt.ciTarget & "." & valueToString(row["$key"]) else: ""
ctx.btrees[colKey].insert(idxVal, IndexEntry(lsmKey: lsmKey, rowValue: ""))
# Persist reconstructed DDL so restoreEngines can rebuild the index
# from table data after a restart (replay re-writes the same key).
# Unnamed indexes: persist the nameless form (see FTS branch above).
# The CREATE INDEX AST does not track UNIQUE, so it is not preserved.
let btreeDdl = if stmt.ciName.len > 0:
"CREATE INDEX " & idxName & " ON " & stmt.ciTarget & " (" & stmt.ciColumns.join(", ") & ")"
else:
"CREATE INDEX ON " & stmt.ciTarget & " (" & stmt.ciColumns.join(", ") & ")"
ctx.db.put(SchemaBtreeIndexPrefix & colKey, cast[seq[byte]](btreeDdl))
return okResult(msg="CREATE INDEX " & idxName & " on " & stmt.ciTarget)
of nkDropIndex:
@@ -1400,8 +1410,16 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
targetKey = key
found = true
break
# A custom index name only appears in the persisted DDL — match it
# against the stored "CREATE INDEX <name> ON" text as well.
let (hasDdl, ddl) = ctx.db.get(SchemaBtreeIndexPrefix & key)
if hasDdl and cast[string](ddl).startsWith("CREATE INDEX " & stmt.diName & " ON "):
targetKey = key
found = true
break
if found:
ctx.btrees.del(targetKey)
ctx.db.delete(SchemaBtreeIndexPrefix & targetKey)
return okResult(msg="DROP INDEX " & stmt.diName)
# FTS/HNSW engine indexes: in-memory maps are keyed by table.col, and a
# custom index name only appears in the persisted DDL — match it against
@@ -1633,14 +1651,15 @@ proc executeMigrationSql(ctx: ExecutionContext, sql: string): ExecResult =
return okResult(msg="Empty migration body")
proc restoreEngines*(ctx: ExecutionContext) =
## Rebuild ephemeral engines (FTS/HNSW indexes, graphs) from persisted
## Rebuild ephemeral engines (B-tree/FTS/HNSW indexes, graphs) from persisted
## schema keys after restoreSchema. Invoked via context.restoreEnginesHook
## at the end of newExecutionContext. Index replay re-persists the same
## key, so it is idempotent.
var ddls: seq[string] = @[]
for (key, value) in ctx.db.scanAll():
if not key.startsWith(SchemaFtsIndexPrefix) and
not key.startsWith(SchemaVecIndexPrefix): continue
not key.startsWith(SchemaVecIndexPrefix) and
not key.startsWith(SchemaBtreeIndexPrefix): continue
let ddl = cast[string](value)
if ddl.len == 0: continue
ddls.add(ddl)
+78
View File
@@ -4,6 +4,7 @@ import std/os
import std/strutils
import std/tables
import barabadb/storage/lsm
import barabadb/storage/btree
import barabadb/query/executor
import barabadb/query/parser
import barabadb/fts/engine
@@ -363,6 +364,83 @@ suite "Schema persistence":
db2.close()
removeDir(dir)
test "B-tree index survives reopen":
let dir = "/tmp/baradb_schema_persist_btree"
removeDir(dir)
block:
var db = newLSMTree(dir)
var ctx = newExecutionContext(db)
check execSql(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)").success
check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)").success
check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25)").success
check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (3, 'Carol', 40)").success
check execSql(ctx, "CREATE INDEX users_age ON users (age)").success
check "users.age" in ctx.btrees
db.close()
# Reopen fresh context (simulates process restart)
block:
var db2 = newLSMTree(dir)
var ctx2 = newExecutionContext(db2)
check "users.age" in ctx2.btrees
let r = execSql(ctx2, "SELECT id FROM users WHERE age = 25")
check r.success
check r.rows.len == 1
# index keeps updating after reopen
check execSql(ctx2, "INSERT INTO users (id, name, age) VALUES (4, 'Dan', 55)").success
if "users.age" in ctx2.btrees:
check ctx2.btrees["users.age"].get("55").len >= 1
db2.close()
removeDir(dir)
test "Unnamed B-tree index survives reopen":
let dir = "/tmp/baradb_schema_persist_btree_noname"
removeDir(dir)
block:
var db = newLSMTree(dir)
var ctx = newExecutionContext(db)
check execSql(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)").success
check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)").success
# No index name — idxName defaults to colKey (users.age) at execution
check execSql(ctx, "CREATE INDEX ON users (age)").success
check "users.age" in ctx.btrees
db.close()
# Reopen fresh context (simulates process restart)
block:
var db2 = newLSMTree(dir)
var ctx2 = newExecutionContext(db2)
# Persisted DDL must be replayable — the dotted fallback name is not.
check "users.age" in ctx2.btrees
let r = execSql(ctx2, "SELECT id FROM users WHERE age = 30")
check r.success
check r.rows.len == 1
db2.close()
removeDir(dir)
test "DROP INDEX removes B-tree index and its schema key":
let dir = "/tmp/baradb_schema_persist_dropbtree"
removeDir(dir)
block:
var db = newLSMTree(dir)
var ctx = newExecutionContext(db)
check execSql(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)").success
check execSql(ctx, "INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)").success
check execSql(ctx, "CREATE INDEX users_age ON users (age)").success
check "users.age" in ctx.btrees
let (foundBefore, _) = db.get(SchemaBtreeIndexPrefix & "users.age")
check foundBefore
let d = execSql(ctx, "DROP INDEX users_age")
check d.success
check "users.age" notin ctx.btrees
let (found, _) = db.get(SchemaBtreeIndexPrefix & "users.age")
check not found
db.close()
block:
var db2 = newLSMTree(dir)
var ctx2 = newExecutionContext(db2)
check "users.age" notin ctx2.btrees # no ghost rebuild
db2.close()
removeDir(dir)
test "Stable schema key format":
check tableSchemaKey("users") == "_schema:tables:users"
check serializeTableDdl(TableDef(