e8f9cbc5bb
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
475 lines
20 KiB
Nim
475 lines
20 KiB
Nim
## Schema persistence — CREATE TABLE / data survive reopen
|
|
import std/unittest
|
|
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
|
|
import barabadb/vector/engine as vengine
|
|
import barabadb/graph/engine as gengine
|
|
|
|
proc execSql(ctx: ExecutionContext, sql: string): ExecResult =
|
|
let node = parse(sql)
|
|
result = executeQuery(ctx, node)
|
|
|
|
suite "Schema persistence":
|
|
test "CREATE TABLE survives flush + reopen":
|
|
let dir = "/tmp/baradb_schema_persist_1"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir, 1024) # small memtable → forces flush
|
|
var ctx = newExecutionContext(db)
|
|
let r = execSql(ctx, "CREATE TABLE users (id INT PRIMARY KEY, name TEXT NOT NULL)")
|
|
check r.success
|
|
check ctx.tables.hasKey("users")
|
|
check ctx.tables["users"].columns.len == 2
|
|
discard execSql(ctx, "INSERT INTO users (id, name) VALUES (1, 'Alice')")
|
|
discard execSql(ctx, "INSERT INTO users (id, name) VALUES (2, 'Bob')")
|
|
db.flush()
|
|
# Schema key must be durable
|
|
let (found, _) = db.get(tableSchemaKey("users"))
|
|
check found
|
|
db.close()
|
|
|
|
# Reopen fresh context (simulates process restart)
|
|
block:
|
|
var db2 = newLSMTree(dir, 1024)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check ctx2.tables.hasKey("users")
|
|
check ctx2.tables["users"].columns.len == 2
|
|
check ctx2.tables["users"].pkColumns.len == 1
|
|
let sel = execSql(ctx2, "SELECT id, name FROM users ORDER BY id")
|
|
check sel.success
|
|
check sel.rows.len == 2
|
|
db2.close()
|
|
|
|
test "DROP TABLE removes schema and data":
|
|
let dir = "/tmp/baradb_schema_persist_drop"
|
|
removeDir(dir)
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE t (id INT PRIMARY KEY)").success
|
|
check execSql(ctx, "INSERT INTO t (id) VALUES (1)").success
|
|
check execSql(ctx, "DROP TABLE t").success
|
|
check not ctx.tables.hasKey("t")
|
|
let (found, _) = db.get(tableSchemaKey("t"))
|
|
check not found
|
|
# Reopen — table must not reappear
|
|
db.close()
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check not ctx2.tables.hasKey("t")
|
|
db2.close()
|
|
|
|
test "ALTER TABLE ADD COLUMN is persisted":
|
|
let dir = "/tmp/baradb_schema_persist_alter"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE items (id INT PRIMARY KEY)").success
|
|
check execSql(ctx, "ALTER TABLE items ADD COLUMN label TEXT").success
|
|
check ctx.tables["items"].columns.len == 2
|
|
db.flush()
|
|
db.close()
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check ctx2.tables.hasKey("items")
|
|
check ctx2.tables["items"].columns.len == 2
|
|
var names: seq[string] = @[]
|
|
for c in ctx2.tables["items"].columns:
|
|
names.add(c.name)
|
|
check "label" in names
|
|
db2.close()
|
|
|
|
test "Multiple tables all restored":
|
|
let dir = "/tmp/baradb_schema_persist_multi"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir, 512)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE a (id INT PRIMARY KEY)").success
|
|
check execSql(ctx, "CREATE TABLE b (id INT PRIMARY KEY, a_id INT)").success
|
|
check execSql(ctx, "CREATE TABLE c (name TEXT)").success
|
|
for i in 0..20:
|
|
discard execSql(ctx, "INSERT INTO a (id) VALUES (" & $i & ")")
|
|
db.flush()
|
|
db.close()
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check ctx2.tables.hasKey("a")
|
|
check ctx2.tables.hasKey("b")
|
|
check ctx2.tables.hasKey("c")
|
|
let sel = execSql(ctx2, "SELECT id FROM a")
|
|
check sel.success
|
|
check sel.rows.len == 21
|
|
db2.close()
|
|
|
|
test "FTS index survives reopen":
|
|
let dir = "/tmp/baradb_schema_persist_fts"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
|
|
check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
|
|
check execSql(ctx, "CREATE INDEX docs_fts ON docs (content) USING FTS").success
|
|
check ctx.ftsIndexes.hasKey("docs.content")
|
|
db.close()
|
|
# Reopen fresh context (simulates process restart)
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
# Index must be rebuilt from the persisted schema key — today it is
|
|
# silently missing, so FTS queries return empty results after reopen.
|
|
check ctx2.ftsIndexes.hasKey("docs.content")
|
|
if ctx2.ftsIndexes.hasKey("docs.content"):
|
|
check ctx2.ftsIndexes["docs.content"].search("quick", limit = 10).len >= 1
|
|
let r = execSql(ctx2, "SELECT id FROM docs WHERE content @@ 'quick'")
|
|
check r.success
|
|
check r.rows.len == 1
|
|
# index keeps updating after reopen
|
|
check execSql(ctx2, "INSERT INTO docs (id, content) VALUES (2, 'quick red fox')").success
|
|
if ctx2.ftsIndexes.hasKey("docs.content"):
|
|
check ctx2.ftsIndexes["docs.content"].search("red", limit = 10).len >= 1
|
|
db2.close()
|
|
removeDir(dir)
|
|
|
|
test "HNSW vector index survives reopen":
|
|
let dir = "/tmp/baradb_schema_persist_vec"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE vecs (id INTEGER PRIMARY KEY, embedding TEXT)").success
|
|
check execSql(ctx, "INSERT INTO vecs (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')").success
|
|
check execSql(ctx, "CREATE INDEX vecs_hnsw ON vecs (embedding) USING HNSW").success
|
|
check ctx.vectorIndexes.hasKey("vecs.embedding")
|
|
db.close()
|
|
# Reopen fresh context (simulates process restart)
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
# Index must be rebuilt from the persisted schema key — today it is
|
|
# silently missing, so vector searches return empty results after reopen.
|
|
check ctx2.vectorIndexes.hasKey("vecs.embedding")
|
|
if ctx2.vectorIndexes.hasKey("vecs.embedding"):
|
|
check vengine.search(ctx2.vectorIndexes["vecs.embedding"],
|
|
@[1.0'f32, 0.0'f32, 0.0'f32], k = 5).len >= 1
|
|
# index keeps updating after reopen
|
|
check execSql(ctx2, "INSERT INTO vecs (id, embedding) VALUES (2, '[0.0, 1.0, 0.0]')").success
|
|
if ctx2.vectorIndexes.hasKey("vecs.embedding"):
|
|
check vengine.search(ctx2.vectorIndexes["vecs.embedding"],
|
|
@[0.0'f32, 1.0'f32, 0.0'f32], k = 5).len >= 1
|
|
db2.close()
|
|
removeDir(dir)
|
|
|
|
test "Unnamed FTS index survives reopen":
|
|
let dir = "/tmp/baradb_schema_persist_fts_noname"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
|
|
check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
|
|
# No index name — idxName defaults to colKey (docs.content) at execution
|
|
check execSql(ctx, "CREATE INDEX ON docs (content) USING FTS").success
|
|
check ctx.ftsIndexes.hasKey("docs.content")
|
|
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 ctx2.ftsIndexes.hasKey("docs.content")
|
|
if ctx2.ftsIndexes.hasKey("docs.content"):
|
|
check ctx2.ftsIndexes["docs.content"].search("quick", limit = 10).len >= 1
|
|
# Nameless persisted DDL must not confuse DROP INDEX name matching
|
|
let d = execSql(ctx2, "DROP INDEX content")
|
|
check d.success
|
|
check "docs.content" notin ctx2.ftsIndexes
|
|
let (found, _) = db2.get(SchemaFtsIndexPrefix & "docs.content")
|
|
check not found
|
|
db2.close()
|
|
block:
|
|
var db3 = newLSMTree(dir)
|
|
var ctx3 = newExecutionContext(db3)
|
|
check "docs.content" notin ctx3.ftsIndexes # no ghost rebuild
|
|
db3.close()
|
|
removeDir(dir)
|
|
|
|
test "Unnamed HNSW index survives reopen":
|
|
let dir = "/tmp/baradb_schema_persist_vec_noname"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE vecs (id INTEGER PRIMARY KEY, embedding TEXT)").success
|
|
check execSql(ctx, "INSERT INTO vecs (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')").success
|
|
# No index name — idxName defaults to colKey (vecs.embedding)
|
|
check execSql(ctx, "CREATE INDEX ON vecs (embedding) USING HNSW").success
|
|
check ctx.vectorIndexes.hasKey("vecs.embedding")
|
|
db.close()
|
|
# Reopen fresh context (simulates process restart)
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check ctx2.vectorIndexes.hasKey("vecs.embedding")
|
|
if ctx2.vectorIndexes.hasKey("vecs.embedding"):
|
|
check vengine.search(ctx2.vectorIndexes["vecs.embedding"],
|
|
@[1.0'f32, 0.0'f32, 0.0'f32], k = 5).len >= 1
|
|
let d = execSql(ctx2, "DROP INDEX embedding")
|
|
check d.success
|
|
check "vecs.embedding" notin ctx2.vectorIndexes
|
|
let (found, _) = db2.get(SchemaVecIndexPrefix & "vecs.embedding")
|
|
check not found
|
|
db2.close()
|
|
block:
|
|
var db3 = newLSMTree(dir)
|
|
var ctx3 = newExecutionContext(db3)
|
|
check "vecs.embedding" notin ctx3.vectorIndexes # no ghost rebuild
|
|
db3.close()
|
|
removeDir(dir)
|
|
|
|
test "Graph survives reopen":
|
|
let dir = "/tmp/baradb_schema_persist_graph"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE GRAPH social").success
|
|
check execSql(ctx, "INSERT INTO social_nodes (id, node_label) VALUES (1, 'person')").success
|
|
check execSql(ctx, "INSERT INTO social_nodes (id, node_label) VALUES (2, 'person')").success
|
|
check execSql(ctx, "INSERT INTO social_edges (source_id, dest_id, edge_label, weight) VALUES (1, 2, 'knows', 1.0)").success
|
|
check "social" in ctx.graphs
|
|
db.close()
|
|
# Reopen fresh context (simulates process restart)
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
# Graph must be rebuilt from the backing tables — today it is
|
|
# silently missing after reopen.
|
|
check "social" in ctx2.graphs
|
|
if "social" in ctx2.graphs:
|
|
check gengine.nodeCount(ctx2.graphs["social"]) == 2
|
|
check gengine.edgeCount(ctx2.graphs["social"]) == 1
|
|
db2.close()
|
|
removeDir(dir)
|
|
|
|
test "DROP INDEX removes FTS index and its schema key (custom name)":
|
|
let dir = "/tmp/baradb_schema_persist_dropfts"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
|
|
check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
|
|
check execSql(ctx, "CREATE INDEX docs_fts ON docs (content) USING FTS").success
|
|
check ctx.ftsIndexes.hasKey("docs.content")
|
|
# Drop by the custom index name — the in-memory key is table.col
|
|
let d = execSql(ctx, "DROP INDEX docs_fts")
|
|
check d.success
|
|
check "docs.content" notin ctx.ftsIndexes
|
|
let (found, _) = db.get(SchemaFtsIndexPrefix & "docs.content")
|
|
check not found
|
|
db.close()
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check "docs.content" notin ctx2.ftsIndexes # no ghost rebuild
|
|
db2.close()
|
|
removeDir(dir)
|
|
|
|
test "DROP INDEX removes FTS index by column key":
|
|
let dir = "/tmp/baradb_schema_persist_dropfts_col"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
|
|
check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
|
|
# No custom name — idxName defaults to colKey (docs.content)
|
|
check execSql(ctx, "CREATE INDEX ON docs (content) USING FTS").success
|
|
check ctx.ftsIndexes.hasKey("docs.content")
|
|
# Drop by column name — matches endsWith(".content")
|
|
let d = execSql(ctx, "DROP INDEX content")
|
|
check d.success
|
|
check "docs.content" notin ctx.ftsIndexes
|
|
let (found, _) = db.get(SchemaFtsIndexPrefix & "docs.content")
|
|
check not found
|
|
db.close()
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check "docs.content" notin ctx2.ftsIndexes # no ghost rebuild
|
|
db2.close()
|
|
removeDir(dir)
|
|
|
|
test "DROP INDEX removes HNSW index and its schema key":
|
|
let dir = "/tmp/baradb_schema_persist_drophnsw"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE vecs (id INTEGER PRIMARY KEY, embedding TEXT)").success
|
|
check execSql(ctx, "INSERT INTO vecs (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')").success
|
|
check execSql(ctx, "CREATE INDEX vecs_hnsw ON vecs (embedding) USING HNSW").success
|
|
check ctx.vectorIndexes.hasKey("vecs.embedding")
|
|
let d = execSql(ctx, "DROP INDEX vecs_hnsw")
|
|
check d.success
|
|
check "vecs.embedding" notin ctx.vectorIndexes
|
|
let (found, _) = db.get(SchemaVecIndexPrefix & "vecs.embedding")
|
|
check not found
|
|
db.close()
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check "vecs.embedding" notin ctx2.vectorIndexes # no ghost rebuild
|
|
db2.close()
|
|
removeDir(dir)
|
|
|
|
test "DROP TABLE removes engine indexes for that table":
|
|
let dir = "/tmp/baradb_schema_persist_droptbl"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)").success
|
|
check execSql(ctx, "INSERT INTO docs (id, content) VALUES (1, 'quick brown fox')").success
|
|
check execSql(ctx, "CREATE INDEX docs_fts ON docs (content) USING FTS").success
|
|
check execSql(ctx, "CREATE TABLE vecs (id INTEGER PRIMARY KEY, embedding TEXT)").success
|
|
check execSql(ctx, "INSERT INTO vecs (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')").success
|
|
check execSql(ctx, "CREATE INDEX vecs_hnsw ON vecs (embedding) USING HNSW").success
|
|
check ctx.ftsIndexes.hasKey("docs.content")
|
|
check ctx.vectorIndexes.hasKey("vecs.embedding")
|
|
check execSql(ctx, "DROP TABLE docs").success
|
|
check "docs.content" notin ctx.ftsIndexes
|
|
check execSql(ctx, "DROP TABLE vecs").success
|
|
check "vecs.embedding" notin ctx.vectorIndexes
|
|
let (ftsFound, _) = db.get(SchemaFtsIndexPrefix & "docs.content")
|
|
check not ftsFound
|
|
let (vecFound, _) = db.get(SchemaVecIndexPrefix & "vecs.embedding")
|
|
check not vecFound
|
|
db.close()
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
check "docs.content" notin ctx2.ftsIndexes # no ghost rebuild
|
|
check "vecs.embedding" notin ctx2.vectorIndexes
|
|
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 "UNIQUE B-tree index survives reopen and still enforces":
|
|
let dir = "/tmp/baradb_schema_persist_unique"
|
|
removeDir(dir)
|
|
block:
|
|
var db = newLSMTree(dir)
|
|
var ctx = newExecutionContext(db)
|
|
check execSql(ctx, "CREATE TABLE accts (id INTEGER PRIMARY KEY, email TEXT)").success
|
|
check execSql(ctx, "CREATE UNIQUE INDEX accts_email ON accts (email)").success
|
|
check execSql(ctx, "INSERT INTO accts (id, email) VALUES (1, 'a@b.c')").success
|
|
check "accts.email" in ctx.btrees
|
|
db.close()
|
|
# Reopen fresh context (simulates process restart)
|
|
block:
|
|
var db2 = newLSMTree(dir)
|
|
var ctx2 = newExecutionContext(db2)
|
|
# UNIQUE flag must survive restart via the persisted DDL.
|
|
check "accts.email" in ctx2.btrees
|
|
let dup = execSql(ctx2, "INSERT INTO accts (id, email) VALUES (2, 'a@b.c')")
|
|
check not dup.success
|
|
let ok = execSql(ctx2, "INSERT INTO accts (id, email) VALUES (2, 'x@y.z')")
|
|
check ok.success
|
|
db2.close()
|
|
removeDir(dir)
|
|
|
|
test "Stable schema key format":
|
|
check tableSchemaKey("users") == "_schema:tables:users"
|
|
check serializeTableDdl(TableDef(
|
|
name: "t",
|
|
columns: @[ColumnDef(name: "id", colType: "INT", isPk: true)],
|
|
pkColumns: @["id"],
|
|
)).contains("PRIMARY KEY")
|