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:" SchemaFtsIndexPrefix* = "_schema:ftsidx:"
SchemaVecIndexPrefix* = "_schema:vecidx:" SchemaVecIndexPrefix* = "_schema:vecidx:"
SchemaGraphsPrefix* = "_schema:graphs:" SchemaGraphsPrefix* = "_schema:graphs:"
SchemaBtreeIndexPrefix* = "_schema:btreeidx:"
## Legacy CREATE TABLE keys (pre-fix) used a migrations: counter suffix ## Legacy CREATE TABLE keys (pre-fix) used a migrations: counter suffix
SchemaLegacyCreatePrefix* = "_schema:migrations:" SchemaLegacyCreatePrefix* = "_schema:migrations:"
+23 -4
View File
@@ -878,11 +878,12 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
dataKeys.add(key) dataKeys.add(key)
for key in dataKeys: for key in dataKeys:
ctx.db.delete(key) 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] = @[] var engineKeys: seq[string] = @[]
for (key, _) in ctx.db.scanAll(): for (key, _) in ctx.db.scanAll():
if key.startsWith(SchemaFtsIndexPrefix & prefix) or if key.startsWith(SchemaFtsIndexPrefix & prefix) or
key.startsWith(SchemaVecIndexPrefix & prefix): key.startsWith(SchemaVecIndexPrefix & prefix) or
key.startsWith(SchemaBtreeIndexPrefix & prefix):
engineKeys.add(key) engineKeys.add(key)
for key in engineKeys: for key in engineKeys:
ctx.db.delete(key) 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): if idxVal.len > 0 and not isNull(idxVal):
let lsmKey = if "$key" in row: stmt.ciTarget & "." & valueToString(row["$key"]) else: "" let lsmKey = if "$key" in row: stmt.ciTarget & "." & valueToString(row["$key"]) else: ""
ctx.btrees[colKey].insert(idxVal, IndexEntry(lsmKey: lsmKey, rowValue: "")) 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) return okResult(msg="CREATE INDEX " & idxName & " on " & stmt.ciTarget)
of nkDropIndex: of nkDropIndex:
@@ -1400,8 +1410,16 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
targetKey = key targetKey = key
found = true found = true
break 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: if found:
ctx.btrees.del(targetKey) ctx.btrees.del(targetKey)
ctx.db.delete(SchemaBtreeIndexPrefix & targetKey)
return okResult(msg="DROP INDEX " & stmt.diName) return okResult(msg="DROP INDEX " & stmt.diName)
# FTS/HNSW engine indexes: in-memory maps are keyed by table.col, and a # 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 # 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") return okResult(msg="Empty migration body")
proc restoreEngines*(ctx: ExecutionContext) = 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 ## schema keys after restoreSchema. Invoked via context.restoreEnginesHook
## at the end of newExecutionContext. Index replay re-persists the same ## at the end of newExecutionContext. Index replay re-persists the same
## key, so it is idempotent. ## key, so it is idempotent.
var ddls: seq[string] = @[] var ddls: seq[string] = @[]
for (key, value) in ctx.db.scanAll(): for (key, value) in ctx.db.scanAll():
if not key.startsWith(SchemaFtsIndexPrefix) and 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) let ddl = cast[string](value)
if ddl.len == 0: continue if ddl.len == 0: continue
ddls.add(ddl) ddls.add(ddl)
+78
View File
@@ -4,6 +4,7 @@ import std/os
import std/strutils import std/strutils
import std/tables import std/tables
import barabadb/storage/lsm import barabadb/storage/lsm
import barabadb/storage/btree
import barabadb/query/executor import barabadb/query/executor
import barabadb/query/parser import barabadb/query/parser
import barabadb/fts/engine import barabadb/fts/engine
@@ -363,6 +364,83 @@ suite "Schema persistence":
db2.close() db2.close()
removeDir(dir) 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": test "Stable schema key format":
check tableSchemaKey("users") == "_schema:tables:users" check tableSchemaKey("users") == "_schema:tables:users"
check serializeTableDdl(TableDef( check serializeTableDdl(TableDef(