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
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:
@@ -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:"
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user