1ed97fb075
CI / test (push) Has been cancelled
CI / raft-e2e (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
Close the remaining 2026-08 findings: peer TLS on leader forward, disttxn SO_ERROR, compaction catalog order, OFFSET without LIMIT, window aggregates, WebSocket mask/size/auth, SCRAM timing and cbind, B-tree leaf left-max separators, and SQL three-valued NULL comparisons.
949 lines
40 KiB
Nim
949 lines
40 KiB
Nim
import std/unittest
|
|
import std/strutils
|
|
import std/os
|
|
import std/tables
|
|
import ../src/barabadb/query/[parser, executor, lexer, ast]
|
|
import ../src/barabadb/query/exec/params
|
|
import ../src/barabadb/query/exec/dml
|
|
import ../src/barabadb/core/types
|
|
import ../src/barabadb/core/config
|
|
import ../src/barabadb/core/replication
|
|
import ../src/barabadb/core/disttxn
|
|
import ../src/barabadb/core/websocket
|
|
import ../src/barabadb/protocol/auth
|
|
import ../src/barabadb/protocol/scram
|
|
import ../src/barabadb/storage/lsm
|
|
import ../src/barabadb/storage/compaction
|
|
import ../src/barabadb/storage/btree
|
|
import std/random
|
|
import std/sets
|
|
|
|
const testDir = "/tmp/baradb_bugfix_test"
|
|
|
|
proc setupCtx(): ExecutionContext =
|
|
removeDir(testDir)
|
|
createDir(testDir)
|
|
let db = newLSMTree(testDir)
|
|
var ctx = newExecutionContext(db)
|
|
# Create tables
|
|
discard executeQuery(ctx, parse("""
|
|
CREATE TABLE thread (id INTEGER PRIMARY KEY, name TEXT, category INTEGER, isDeleted INTEGER)
|
|
"""))
|
|
discard executeQuery(ctx, parse("""
|
|
CREATE TABLE category (id INTEGER PRIMARY KEY, name TEXT, description TEXT)
|
|
"""))
|
|
discard executeQuery(ctx, parse("""
|
|
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)
|
|
"""))
|
|
ctx
|
|
|
|
proc teardown(ctx: ExecutionContext) =
|
|
ctx.db.close()
|
|
removeDir(testDir)
|
|
|
|
suite "Bug fixes — IN list, nkPath exprToSql, multi-table joins":
|
|
|
|
test "IN (list) parses without error":
|
|
let sql = "SELECT id, name FROM users WHERE id IN (1, 2, 3)"
|
|
let tokens = tokenize(sql)
|
|
let ast = parse(tokens)
|
|
check ast.stmts[0].kind == nkSelect
|
|
let whereExpr = ast.stmts[0].selWhere.whereExpr
|
|
check whereExpr.kind == nkInExpr
|
|
check whereExpr.inRight.kind == nkArrayLit
|
|
check whereExpr.inRight.arrayElems.len == 3
|
|
|
|
test "IN (list) with strings parses":
|
|
let sql = "SELECT * FROM users WHERE name IN ('alice', 'bob', 'charlie')"
|
|
let tokens = tokenize(sql)
|
|
let ast = parse(tokens)
|
|
let whereExpr = ast.stmts[0].selWhere.whereExpr
|
|
check whereExpr.kind == nkInExpr
|
|
check whereExpr.inRight.kind == nkArrayLit
|
|
check whereExpr.inRight.arrayElems.len == 3
|
|
|
|
test "IN (subquery) still parses":
|
|
let sql = "SELECT id FROM users WHERE id IN (SELECT id FROM admins)"
|
|
let tokens = tokenize(sql)
|
|
let ast = parse(tokens)
|
|
let whereExpr = ast.stmts[0].selWhere.whereExpr
|
|
check whereExpr.kind == nkInExpr
|
|
check whereExpr.inRight.kind == nkSubquery
|
|
|
|
test "nkPath column alias in SELECT produces correct data":
|
|
let sql = "SELECT t.id FROM posts t"
|
|
let tokens = tokenize(sql)
|
|
let ast = parse(tokens)
|
|
let selExpr = ast.stmts[0].selResult[0]
|
|
check selExpr.kind == nkPath
|
|
check selExpr.pathParts == @["t", "id"]
|
|
# Verify correct data is returned
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO posts (id, title) VALUES (42, 'Hello')"))
|
|
let r = executeQuery(ctx, parse(sql))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
# Column is "t.id" (qualified by alias)
|
|
check valueToString(r.rows[0]["t.id"]) == "42"
|
|
|
|
test "IN (list) executes with actual data":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (3, 'charlie')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (4, 'dave')"))
|
|
|
|
let r = executeQuery(ctx, parse("SELECT id, name FROM users WHERE id IN (1, 3, 4)"))
|
|
check r.success
|
|
check r.rows.len == 3
|
|
|
|
test "Multi-table implicit join without aliases (Bug 1)":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO thread (id, name, category, isDeleted) VALUES (3, 'Test Thread', 1, 0)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO category (id, name, description) VALUES (1, 'General', 'General discussion')"))
|
|
|
|
# This used to return 0 rows
|
|
let r = executeQuery(ctx, parse("SELECT thread.id, thread.name, category.id, category.name FROM thread, category WHERE thread.id = 3 AND thread.isDeleted = 0 AND thread.category = category.id"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
|
|
test "Multi-table join with aliases still works":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO thread (id, name, category, isDeleted) VALUES (3, 'Test Thread', 1, 0)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO category (id, name, description) VALUES (1, 'General', 'General discussion')"))
|
|
|
|
let r = executeQuery(ctx, parse("SELECT t.id AS thread_id, t.name AS thread_name, c.id AS cat_id, c.name AS cat_name FROM thread t, category c WHERE t.id = 3 AND t.category = c.id"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check r.columns == @["thread_id", "thread_name", "cat_id", "cat_name"]
|
|
|
|
test "Three-table join without aliases (Bug 4 - nkPath in column names)":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE post (id INTEGER PRIMARY KEY, author INTEGER, thread INTEGER, creation TEXT)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO thread (id, name, category, isDeleted) VALUES (5, 'Thread5', 1, 0)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO category (id, name, description) VALUES (1, 'Cat1', 'desc1')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (10, 'Alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO post (id, author, thread, creation) VALUES (100, 10, 5, '2024-06-01 12:00:00')"))
|
|
|
|
# Three-table join with nkPath references
|
|
let r = executeQuery(ctx, parse("SELECT post.id, post.creation, post.thread, users.id, users.name FROM post, users, thread WHERE post.thread = thread.id AND post.author = users.id AND post.id = 100"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
# Verify qualified column references resolve correctly
|
|
check valueToString(r.rows[0]["post.id"]) == "100"
|
|
check valueToString(r.rows[0]["users.name"]) == "Alice"
|
|
check valueToString(r.rows[0]["post.thread"]) == "5"
|
|
# Check column names don't contain "nkPath"
|
|
for col in r.columns:
|
|
check not col.contains("nkPath")
|
|
|
|
test "NOT IN (list) parses and executes":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (3, 'charlie')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (4, 'dave')"))
|
|
|
|
let r = executeQuery(ctx, parse("SELECT id, name FROM users WHERE id NOT IN (1, 4)"))
|
|
check r.success
|
|
check r.rows.len == 2
|
|
|
|
test "NOT LIKE parses and executes":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (3, 'charlie')"))
|
|
|
|
let r = executeQuery(ctx, parse("SELECT name FROM users WHERE name NOT LIKE 'a%'"))
|
|
check r.success
|
|
check r.rows.len == 2
|
|
|
|
test "NOT BETWEEN parses and executes":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (5, 'bob')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (10, 'charlie')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (15, 'dave')"))
|
|
|
|
let r = executeQuery(ctx, parse("SELECT id, name FROM users WHERE id NOT BETWEEN 3 AND 12"))
|
|
check r.success
|
|
check r.rows.len == 2
|
|
|
|
test "Multi-column ORDER BY sorts correctly":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (3, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (4, 'charlie')"))
|
|
|
|
let r = executeQuery(ctx, parse("SELECT name, id FROM users ORDER BY name ASC, id DESC"))
|
|
check r.success
|
|
check r.rows.len == 4
|
|
# After sorting by name ASC, then id DESC within same name:
|
|
# alice(3), alice(1), bob(2), charlie(4)
|
|
check valueToString(r.rows[0]["id"]) == "3"
|
|
check valueToString(r.rows[1]["id"]) == "1"
|
|
check valueToString(r.rows[2]["id"]) == "2"
|
|
check valueToString(r.rows[3]["id"]) == "4"
|
|
|
|
test "Numeric != comparison is consistent with =":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
# 5.0 stored as string should equal integer 5 via numeric comparison
|
|
let r = executeQuery(ctx, parse("SELECT id, name FROM users WHERE id != 999"))
|
|
check r.success
|
|
# Should return all rows since none have id=999
|
|
|
|
test "Column alias consistency for table.column in getSelectColumns":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO posts (id, title) VALUES (1, 'Hello')"))
|
|
|
|
let r = executeQuery(ctx, parse("SELECT posts.id, posts.title FROM posts"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
# Column names should match the full path "posts.id", "posts.title"
|
|
check r.columns == @["posts.id", "posts.title"]
|
|
# Data should be accessible via both qualified and unqualified names
|
|
check valueToString(r.rows[0]["posts.id"]) == "1"
|
|
|
|
test "DELETE inside transaction actually removes row":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (10, 'temp')"))
|
|
# Verify row exists
|
|
let r1 = executeQuery(ctx, parse("SELECT * FROM users WHERE id = 10"))
|
|
check r1.success
|
|
check r1.rows.len == 1
|
|
# Delete within transaction
|
|
discard executeQuery(ctx, parse("BEGIN"))
|
|
let del = executeQuery(ctx, parse("DELETE FROM users WHERE id = 10"))
|
|
check del.success
|
|
discard executeQuery(ctx, parse("COMMIT"))
|
|
# Verify row is gone
|
|
let r2 = executeQuery(ctx, parse("SELECT * FROM users WHERE id = 10"))
|
|
check r2.success
|
|
check r2.rows.len == 0
|
|
|
|
test "PK-only row survives transaction commit":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE pksimple (id INTEGER PRIMARY KEY)"))
|
|
discard executeQuery(ctx, parse("BEGIN"))
|
|
discard executeQuery(ctx, parse("INSERT INTO pksimple (id) VALUES (1)"))
|
|
discard executeQuery(ctx, parse("COMMIT"))
|
|
let r = executeQuery(ctx, parse("SELECT * FROM pksimple WHERE id = 1"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
|
|
test "MIN and MAX skip NULL values":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE scores (id INTEGER PRIMARY KEY, val INTEGER)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO scores (id, val) VALUES (1, 10)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO scores (id, val) VALUES (2, NULL)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO scores (id, val) VALUES (3, 30)"))
|
|
let rmin = executeQuery(ctx, parse("SELECT MIN(val) AS m FROM scores"))
|
|
check rmin.success
|
|
check rmin.rows.len == 1
|
|
check valueToString(rmin.rows[0]["m"]) == "10"
|
|
let rmax = executeQuery(ctx, parse("SELECT MAX(val) AS m FROM scores"))
|
|
check rmax.success
|
|
check rmax.rows.len == 1
|
|
check valueToString(rmax.rows[0]["m"]) == "30"
|
|
|
|
suite "Bug fixes — keyword 'header' usable as column name":
|
|
|
|
test "CREATE TABLE / INSERT / SELECT with column named 'header'":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
# nimforum schema uses a column named 'header' (tkHeader is the CSV IMPORT keyword)
|
|
let c = executeQuery(ctx, parse("CREATE TABLE post (id INTEGER PRIMARY KEY, header TEXT, content TEXT)"))
|
|
check c.success
|
|
let i = executeQuery(ctx, parse("INSERT INTO post (id, header, content) VALUES (1, 'Hello', 'World')"))
|
|
check i.success
|
|
let r = executeQuery(ctx, parse("SELECT header FROM post WHERE id = 1"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check valueToString(r.rows[0]["header"]) == "Hello"
|
|
let rw = executeQuery(ctx, parse("SELECT id FROM post WHERE header = 'Hello'"))
|
|
check rw.success
|
|
check rw.rows.len == 1
|
|
|
|
test "IMPORT ... HEADER clause still parses after soft-keyword change":
|
|
let ast = parse("IMPORT FROM 'data.csv' INTO post HEADER no")
|
|
check ast.stmts.len == 1
|
|
check ast.stmts[0].kind == nkImportFrom
|
|
check ast.stmts[0].impHasHeader == false
|
|
let ast2 = parse("EXPORT TO 'out.csv' FROM post HEADER yes")
|
|
check ast2.stmts.len == 1
|
|
check ast2.stmts[0].kind == nkExportTo
|
|
check ast2.stmts[0].expIncludeHeader == true
|
|
|
|
suite "Bug fixes — clause keywords usable as identifiers":
|
|
|
|
test "columns named after clause keywords (format, status, user, ...)":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
let c = executeQuery(ctx, parse("""
|
|
CREATE TABLE kw (id INTEGER PRIMARY KEY, format TEXT, status TEXT, user TEXT,
|
|
batch INTEGER, csv TEXT, ndjson TEXT, delimiter TEXT,
|
|
migration TEXT, apply TEXT, up TEXT, down TEXT, dryrun TEXT,
|
|
policy TEXT, enable TEXT, disable TEXT, recover TEXT,
|
|
before TEXT, after TEXT, instead TEXT, of TEXT)
|
|
"""))
|
|
check c.success
|
|
let i = executeQuery(ctx, parse(
|
|
"INSERT INTO kw (id, format, status, user, batch, of) VALUES (1, 'csv', 'active', 'admin', 7, 'x')"))
|
|
check i.success
|
|
let u = executeQuery(ctx, parse("UPDATE kw SET status = 'done' WHERE id = 1"))
|
|
check u.success
|
|
let r = executeQuery(ctx, parse("SELECT format, status, user, batch, of FROM kw WHERE id = 1"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check valueToString(r.rows[0]["format"]) == "csv"
|
|
check valueToString(r.rows[0]["status"]) == "done"
|
|
check valueToString(r.rows[0]["user"]) == "admin"
|
|
check valueToString(r.rows[0]["batch"]) == "7"
|
|
let rq = executeQuery(ctx, parse("SELECT kw.status FROM kw WHERE kw.status = 'done'"))
|
|
check rq.success
|
|
check rq.rows.len == 1
|
|
|
|
test "IMPORT/EXPORT accept keyword values: FORMAT csv/ndjson/json, HEADER true/false":
|
|
let a1 = parse("IMPORT FROM 'd.csv' INTO t FORMAT csv HEADER true")
|
|
check a1.stmts[0].impFormat == "csv"
|
|
check a1.stmts[0].impHasHeader == true
|
|
let a2 = parse("IMPORT FROM 'd.csv' INTO t FORMAT ndjson HEADER false")
|
|
check a2.stmts[0].impFormat == "ndjson"
|
|
check a2.stmts[0].impHasHeader == false
|
|
let a3 = parse("EXPORT TO 'o.csv' FROM t FORMAT json HEADER true")
|
|
check a3.stmts[0].expFormat == "json"
|
|
check a3.stmts[0].expIncludeHeader == true
|
|
let a4 = parse("EXPORT TO 'o.csv' FROM t FORMAT csv DELIMITER ';' HEADER false")
|
|
check a4.stmts[0].expFormat == "csv"
|
|
check a4.stmts[0].expIncludeHeader == false
|
|
|
|
suite "Bug fixes — UNIQUE index enforcement":
|
|
|
|
test "CREATE UNIQUE INDEX rejects duplicate INSERT":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE accts (id INTEGER PRIMARY KEY, email TEXT)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO accts (id, email) VALUES (1, 'a@b.c')"))
|
|
let c = executeQuery(ctx, parse("CREATE UNIQUE INDEX accts_email ON accts (email)"))
|
|
check c.success
|
|
let dup = executeQuery(ctx, parse("INSERT INTO accts (id, email) VALUES (2, 'a@b.c')"))
|
|
check not dup.success
|
|
let ok = executeQuery(ctx, parse("INSERT INTO accts (id, email) VALUES (2, 'x@y.z')"))
|
|
check ok.success
|
|
|
|
test "CREATE UNIQUE INDEX rejects duplicate UPDATE":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE accts (id INTEGER PRIMARY KEY, email TEXT)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO accts (id, email) VALUES (1, 'a@b.c')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO accts (id, email) VALUES (2, 'x@y.z')"))
|
|
let c = executeQuery(ctx, parse("CREATE UNIQUE INDEX accts_email ON accts (email)"))
|
|
check c.success
|
|
let dup = executeQuery(ctx, parse("UPDATE accts SET email = 'a@b.c' WHERE id = 2"))
|
|
check not dup.success
|
|
let same = executeQuery(ctx, parse("UPDATE accts SET email = 'a@b.c' WHERE id = 1"))
|
|
check same.success
|
|
|
|
test "CREATE UNIQUE INDEX over duplicate data fails":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE accts (id INTEGER PRIMARY KEY, email TEXT)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO accts (id, email) VALUES (1, 'a@b.c')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO accts (id, email) VALUES (2, 'a@b.c')"))
|
|
let c = executeQuery(ctx, parse("CREATE UNIQUE INDEX accts_email ON accts (email)"))
|
|
check not c.success
|
|
|
|
suite "Production config gate":
|
|
|
|
test "validateProductionConfig rejects missing secret":
|
|
putEnv("BARADB_ENV", "production")
|
|
defer: delEnv("BARADB_ENV")
|
|
var cfg = defaultConfig()
|
|
cfg.authEnabled = true
|
|
cfg.jwtSecret = ""
|
|
var msg = ""
|
|
try:
|
|
validateProductionConfig(cfg)
|
|
except ValueError as e:
|
|
msg = e.msg
|
|
check "JWT" in msg or "secret" in msg.toLower()
|
|
|
|
test "validateProductionConfig accepts strong secret":
|
|
putEnv("BARADB_ENV", "production")
|
|
defer: delEnv("BARADB_ENV")
|
|
var cfg = defaultConfig()
|
|
cfg.authEnabled = true
|
|
cfg.jwtSecret = "a".repeat(32)
|
|
validateProductionConfig(cfg) # must not raise
|
|
|
|
suite "Raft peer address parsing":
|
|
|
|
test "BARADB_RAFT_CLIENT_PEERS populates raftPeerClientAddrs":
|
|
putEnv("BARADB_RAFT_CLIENT_PEERS", "n1@10.0.0.1:9472,n2@10.0.0.2:9472")
|
|
defer: delEnv("BARADB_RAFT_CLIENT_PEERS")
|
|
var cfg = defaultConfig()
|
|
loadConfigFromEnv(cfg)
|
|
check cfg.raftPeerClientAddrs.len == 2
|
|
check cfg.raftPeerClientAddrs["n1"] == ("10.0.0.1", 9472)
|
|
check cfg.raftPeerClientAddrs["n2"] == ("10.0.0.2", 9472)
|
|
|
|
test "id@host:port entries populate raftPeerAddrs":
|
|
putEnv("BARADB_RAFT_PEERS", "n1@127.0.0.1:9473,n2@10.0.0.5:9474,n3")
|
|
defer: delEnv("BARADB_RAFT_PEERS")
|
|
var cfg = defaultConfig()
|
|
loadConfigFromEnv(cfg)
|
|
check cfg.raftPeers == @["n1", "n2", "n3"]
|
|
check cfg.raftPeerAddrs["n1"] == ("127.0.0.1", 9473)
|
|
check cfg.raftPeerAddrs["n2"] == ("10.0.0.5", 9474)
|
|
check "n3" notin cfg.raftPeerAddrs
|
|
|
|
test "malformed peer entries raise with the entry in the message":
|
|
for bad in ["n1@:9473", "n1@host:notaport", "@host:9473", "n1@host:0", "n1@host:70000"]:
|
|
putEnv("BARADB_RAFT_PEERS", bad)
|
|
var cfg = defaultConfig()
|
|
var msg = ""
|
|
try:
|
|
loadConfigFromEnv(cfg)
|
|
except ValueError as e:
|
|
msg = e.msg
|
|
delEnv("BARADB_RAFT_PEERS")
|
|
check msg.len > 0
|
|
check bad in msg
|
|
|
|
suite "Raft put/delete encoding — empty value is not a delete":
|
|
|
|
test "PK-only INSERT yields a put pair (deleted == false, empty value)":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE pkonly (id INTEGER PRIMARY KEY)"))
|
|
let r = executeQuery(ctx, parse("INSERT INTO pkonly (id) VALUES (1)"))
|
|
check r.success
|
|
check r.keyValuePairs.len == 1
|
|
check r.keyValuePairs[0].value.len == 0
|
|
check r.keyValuePairs[0].deleted == false
|
|
|
|
test "DELETE yields a delete pair (deleted == true)":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
let r = executeQuery(ctx, parse("DELETE FROM users WHERE id = 1"))
|
|
check r.success
|
|
check r.keyValuePairs.len == 1
|
|
check r.keyValuePairs[0].deleted == true
|
|
check r.keyValuePairs[0].value.len == 0
|
|
|
|
test "UPDATE yields a put pair (deleted == false)":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
let r = executeQuery(ctx, parse("UPDATE users SET name = 'bob' WHERE id = 1"))
|
|
check r.success
|
|
check r.keyValuePairs.len == 1
|
|
check r.keyValuePairs[0].deleted == false
|
|
check r.keyValuePairs[0].value.len > 0
|
|
|
|
test "txn COMMIT pairs carry deleted flag for buffered writes":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE pkonly (id INTEGER PRIMARY KEY)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("BEGIN"))
|
|
discard executeQuery(ctx, parse("INSERT INTO pkonly (id) VALUES (7)"))
|
|
discard executeQuery(ctx, parse("DELETE FROM users WHERE id = 1"))
|
|
let r = executeQuery(ctx, parse("COMMIT"))
|
|
check r.success
|
|
check r.keyValuePairs.len == 2
|
|
var sawPut = false
|
|
var sawDelete = false
|
|
for pair in r.keyValuePairs:
|
|
if pair.deleted:
|
|
sawDelete = true
|
|
check pair.value.len == 0
|
|
else:
|
|
sawPut = true
|
|
check pair.key == "pkonly.id=7"
|
|
check pair.value.len == 0 # empty value must still be a put
|
|
check sawPut and sawDelete
|
|
|
|
test "apply of a put with empty value keeps the PK-only row":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE pkonly (id INTEGER PRIMARY KEY)"))
|
|
let r = executeQuery(ctx, parse("INSERT INTO pkonly (id) VALUES (3)"))
|
|
check r.success
|
|
check r.keyValuePairs.len == 1
|
|
let pair = r.keyValuePairs[0]
|
|
# Same decode as applyCommand in src/baradadb.nim for a "put" entry.
|
|
let encoded = pair.key & "\x00" & cast[string](pair.value)
|
|
let parts = encoded.split("\x00")
|
|
check parts.len >= 2
|
|
applyReplicatedPut(ctx, parts[0], cast[seq[byte]](parts[1]))
|
|
let sel = executeQuery(ctx, parse("SELECT * FROM pkonly WHERE id = 3"))
|
|
check sel.success
|
|
check sel.rows.len == 1
|
|
|
|
suite "Raft write classification":
|
|
|
|
test "isWrite classifies DML and COMMIT":
|
|
check isWrite(parse("INSERT INTO t (id) VALUES (1)").stmts[0])
|
|
check isWrite(parse("UPDATE t SET id = 2").stmts[0])
|
|
check isWrite(parse("DELETE FROM t WHERE id = 1").stmts[0])
|
|
check isWrite(parse("COMMIT").stmts[0])
|
|
check not isWrite(parse("SELECT * FROM t").stmts[0])
|
|
check not isWrite(parse("CREATE TABLE t (id INT)").stmts[0])
|
|
check not isWrite(parse("BEGIN").stmts[0])
|
|
check not isWrite(parse("ROLLBACK").stmts[0])
|
|
|
|
test "multi-statement queries with a trailing write are still writes":
|
|
## Server rejection must not look only at stmts[0] — a SELECT first
|
|
## would otherwise let a follower execute the INSERT.
|
|
let ast = parse("SELECT 1; INSERT INTO t (id) VALUES (1)")
|
|
check ast.stmts.len == 2
|
|
check not isWrite(ast.stmts[0])
|
|
check isWrite(ast.stmts[1])
|
|
var anyWrite = false
|
|
for s in ast.stmts:
|
|
if isWrite(s): anyWrite = true
|
|
check anyWrite
|
|
|
|
test "isRaftDdl covers schema but not CREATE DATABASE":
|
|
check isRaftDdl(parse("CREATE TABLE t (id INT)").stmts[0])
|
|
check isRaftDdl(parse("CREATE INDEX i ON t (id)").stmts[0])
|
|
check isRaftDdl(parse("DROP TABLE t").stmts[0])
|
|
check not isRaftDdl(parse("CREATE DATABASE other").stmts[0])
|
|
check not isRaftDdl(parse("INSERT INTO t (id) VALUES (1)").stmts[0])
|
|
check not isRaftDdl(parse("SELECT 1").stmts[0])
|
|
|
|
|
|
suite "Raft TLS config":
|
|
|
|
test "default config has raft TLS disabled with empty paths":
|
|
let cfg = defaultConfig()
|
|
check cfg.raftTlsEnabled == false
|
|
check cfg.raftTlsCertFile == ""
|
|
check cfg.raftTlsKeyFile == ""
|
|
check cfg.raftTlsCaFile == ""
|
|
check cfg.raftTlsVerifyPeer == false
|
|
|
|
test "env vars parse into raft TLS config":
|
|
putEnv("BARADB_RAFT_TLS_ENABLED", "true")
|
|
putEnv("BARADB_RAFT_TLS_CERT_FILE", "/tmp/raft.crt")
|
|
putEnv("BARADB_RAFT_TLS_KEY_FILE", "/tmp/raft.key")
|
|
putEnv("BARADB_RAFT_TLS_CA_FILE", "/tmp/raft-ca.crt")
|
|
putEnv("BARADB_RAFT_TLS_VERIFY_PEER", "1")
|
|
defer:
|
|
delEnv("BARADB_RAFT_TLS_ENABLED")
|
|
delEnv("BARADB_RAFT_TLS_CERT_FILE")
|
|
delEnv("BARADB_RAFT_TLS_KEY_FILE")
|
|
delEnv("BARADB_RAFT_TLS_CA_FILE")
|
|
delEnv("BARADB_RAFT_TLS_VERIFY_PEER")
|
|
var cfg = defaultConfig()
|
|
loadConfigFromEnv(cfg)
|
|
check cfg.raftTlsEnabled == true
|
|
check cfg.raftTlsCertFile == "/tmp/raft.crt"
|
|
check cfg.raftTlsKeyFile == "/tmp/raft.key"
|
|
check cfg.raftTlsCaFile == "/tmp/raft-ca.crt"
|
|
check cfg.raftTlsVerifyPeer == true
|
|
|
|
|
|
suite "Legacy REP payload encoding — empty value is not a delete":
|
|
|
|
test "PK-only put (empty value) round-trips as a put, not a delete":
|
|
## Regression: the legacy REP receiver used to infer a delete from an empty
|
|
## value, so PK-only rows (empty LSM value) vanished on the replica.
|
|
let decoded = decodeRepPayload(encodeRepPayload(false, "pkonly.id=3", @[]))
|
|
check decoded.op == ropPut
|
|
check decoded.key == "pkonly.id=3"
|
|
check decoded.value.len == 0
|
|
|
|
test "delete round-trips as a delete":
|
|
let decoded = decodeRepPayload(encodeRepPayload(true, "users.id=1", @[]))
|
|
check decoded.op == ropDelete
|
|
check decoded.key == "users.id=1"
|
|
check decoded.value.len == 0
|
|
|
|
test "put with a non-empty value preserves the value bytes":
|
|
let decoded = decodeRepPayload(
|
|
encodeRepPayload(false, "users.id=1", cast[seq[byte]]("bob")))
|
|
check decoded.op == ropPut
|
|
check decoded.key == "users.id=1"
|
|
check cast[string](decoded.value) == "bob"
|
|
|
|
test "value containing a null byte survives the round-trip":
|
|
## Decode splits on the FIRST null (the key/value separator) only.
|
|
let value = @[byte('a'), byte(0), byte('b')]
|
|
let decoded = decodeRepPayload(encodeRepPayload(false, "k", value))
|
|
check decoded.op == ropPut
|
|
check decoded.key == "k"
|
|
check decoded.value == value
|
|
|
|
test "empty or untagged payloads decode as invalid, not delete":
|
|
check decodeRepPayload(@[]).op == ropInvalid
|
|
check decodeRepPayload(cast[seq[byte]]("Xfoo")).op == ropInvalid
|
|
|
|
|
|
suite "Query operator correctness — audit batch 1":
|
|
|
|
test "power operator ** evaluates, not lowered to equality":
|
|
## Regression: bkPow used to fall through to `else: irOp = irEq`, so
|
|
## `2 ** 3` evaluated as `2 = 3` (false) instead of 8.
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'a')"))
|
|
let r = executeQuery(ctx, parse("SELECT 2 ** 3 AS x FROM users"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check parseFloat(valueToString(r.rows[0]["x"])) == 8.0
|
|
|
|
test "concat operator ++ concatenates strings":
|
|
## Regression: bkConcat also fell through to irEq, so `'a' ++ 'b'`
|
|
## evaluated as `'a' = 'b'` (false) instead of "ab".
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'a')"))
|
|
let r = executeQuery(ctx, parse("SELECT 'a' ++ 'b' AS x FROM users"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check valueToString(r.rows[0]["x"]) == "ab"
|
|
|
|
test "!= is the complement of = for numerically equal values":
|
|
## Regression: irNeq short-circuited on string inequality, so `1 != 1.0`
|
|
## was true while `1 = 1.0` was also true (not complements).
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
let eq = executeQuery(ctx, parse("SELECT * FROM users WHERE id = 1.0"))
|
|
let neq = executeQuery(ctx, parse("SELECT * FROM users WHERE id != 1.0"))
|
|
check eq.rows.len == 1 # 1 = 1.0 -> true
|
|
check neq.rows.len == 0 # 1 != 1.0 -> false (old bug returned the row)
|
|
|
|
|
|
suite "Query correctness — audit batch 2":
|
|
|
|
test "COUNT(DISTINCT) deduplicates values":
|
|
## Regression: funcDistinct was parsed but never copied to aggDistinct /
|
|
## never consulted during aggregation.
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (3, 'alice')"))
|
|
let r = executeQuery(ctx, parse("SELECT COUNT(DISTINCT name) AS c FROM users"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check valueToString(r.rows[0]["c"]) == "2"
|
|
|
|
test "SUM(DISTINCT) sums unique values only":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'a')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'b')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (5, 'c')"))
|
|
# ids 1, 2, 5 — insert another row with id-like values via a number col
|
|
discard executeQuery(ctx, parse("CREATE TABLE nums (id INTEGER PRIMARY KEY, n INTEGER)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO nums (id, n) VALUES (1, 10)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO nums (id, n) VALUES (2, 10)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO nums (id, n) VALUES (3, 20)"))
|
|
let r = executeQuery(ctx, parse("SELECT SUM(DISTINCT n) AS s FROM nums"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check parseFloat(valueToString(r.rows[0]["s"])) == 30.0
|
|
|
|
test "UNION deduplicates without KeyError":
|
|
## Regression: set-op dedup used row["$value"] which projected rows lack.
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (3, 'alice')"))
|
|
let r = executeQuery(ctx, parse(
|
|
"SELECT name FROM users WHERE id = 1 UNION SELECT name FROM users WHERE id = 3"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check valueToString(r.rows[0]["name"]) == "alice"
|
|
|
|
test "INTERSECT returns common rows":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
let r = executeQuery(ctx, parse(
|
|
"SELECT name FROM users WHERE id <= 2 INTERSECT SELECT name FROM users WHERE id = 1"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check valueToString(r.rows[0]["name"]) == "alice"
|
|
|
|
test "EXCEPT removes right-side rows":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
let r = executeQuery(ctx, parse(
|
|
"SELECT name FROM users EXCEPT SELECT name FROM users WHERE id = 1"))
|
|
check r.success
|
|
check r.rows.len == 1
|
|
check valueToString(r.rows[0]["name"]) == "bob"
|
|
|
|
test "MERGE WHEN MATCHED THEN DELETE removes the row":
|
|
## Regression: mergeMatchedDelete was parsed but never executed.
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("CREATE TABLE inv (id INTEGER PRIMARY KEY, qty INTEGER)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO inv (id, qty) VALUES (1, 10)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO inv (id, qty) VALUES (2, 20)"))
|
|
discard executeQuery(ctx, parse("CREATE TABLE deltas (id INTEGER PRIMARY KEY, qty INTEGER)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO deltas (id, qty) VALUES (1, 0)"))
|
|
let r = executeQuery(ctx, parse("""
|
|
MERGE INTO inv AS t
|
|
USING deltas AS s
|
|
ON t.id = s.id
|
|
WHEN MATCHED THEN DELETE
|
|
"""))
|
|
check r.success
|
|
check r.affectedRows >= 1
|
|
let left = executeQuery(ctx, parse("SELECT id FROM inv ORDER BY id"))
|
|
check left.success
|
|
check left.rows.len == 1
|
|
check valueToString(left.rows[0]["id"]) == "2"
|
|
|
|
test "semi-sync writeLsn returns 0 when replicas do not ack":
|
|
var rm = newReplicationManager(rmSemiSync, syncCount = 1)
|
|
rm.addReplica(newReplica("r1", "10.0.0.1", 9472))
|
|
rm.connectReplica("r1")
|
|
let lsn = rm.writeLsn(@[1'u8, 2, 3])
|
|
check lsn == 0
|
|
|
|
|
|
suite "Audit batch 3 — remaining 2026-08 findings":
|
|
|
|
test "TLS CA file auto-enables peer verify":
|
|
putEnv("BARADB_TLS_CA_FILE", "/tmp/ca.crt")
|
|
defer: delEnv("BARADB_TLS_CA_FILE")
|
|
var cfg = defaultConfig()
|
|
loadConfigFromEnv(cfg)
|
|
check cfg.tlsCaFile == "/tmp/ca.crt"
|
|
check cfg.tlsVerifyPeer == true
|
|
|
|
test "explicit BARADB_TLS_VERIFY_PEER=false wins over CA auto-enable":
|
|
putEnv("BARADB_TLS_CA_FILE", "/tmp/ca.crt")
|
|
putEnv("BARADB_TLS_VERIFY_PEER", "false")
|
|
defer:
|
|
delEnv("BARADB_TLS_CA_FILE")
|
|
delEnv("BARADB_TLS_VERIFY_PEER")
|
|
var cfg = defaultConfig()
|
|
loadConfigFromEnv(cfg)
|
|
check cfg.tlsVerifyPeer == false
|
|
|
|
test "production TLS without verify is rejected":
|
|
putEnv("BARADB_ENV", "production")
|
|
defer: delEnv("BARADB_ENV")
|
|
var cfg = defaultConfig()
|
|
cfg.authEnabled = true
|
|
cfg.jwtSecret = "a".repeat(32)
|
|
cfg.tlsEnabled = true
|
|
var msg = ""
|
|
try:
|
|
validateProductionConfig(cfg)
|
|
except ValueError as e:
|
|
msg = e.msg
|
|
check "TLS" in msg or "verify" in msg.toLower()
|
|
|
|
test "OFFSET without LIMIT returns remaining rows":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'a')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'b')"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (3, 'c')"))
|
|
let r = executeQuery(ctx, parse("SELECT id FROM users ORDER BY id OFFSET 1"))
|
|
check r.success
|
|
check r.rows.len == 2
|
|
check valueToString(r.rows[0]["id"]) == "2"
|
|
check valueToString(r.rows[1]["id"]) == "3"
|
|
|
|
test "LIMIT 0 returns no rows":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'a')"))
|
|
let r = executeQuery(ctx, parse("SELECT id FROM users LIMIT 0"))
|
|
check r.success
|
|
check r.rows.len == 0
|
|
|
|
test "negative LIMIT is clamped to empty, not IndexDefect":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'a')"))
|
|
let r = executeQuery(ctx, parse("SELECT id FROM users LIMIT -5"))
|
|
check r.success
|
|
check r.rows.len == 0
|
|
|
|
test "WebSocket decode rejects unmasked client frames":
|
|
let raw = encodeFrame(0x1, "SUBSCRIBE t", masked = false)
|
|
let (_, n) = decodeFrame(raw)
|
|
check n < 0
|
|
|
|
test "WebSocket decode accepts masked client frames":
|
|
let raw = encodeFrame(0x1, "SUBSCRIBE t", masked = true,
|
|
maskKey = [1'u8, 2, 3, 4])
|
|
let (frame, n) = decodeFrame(raw)
|
|
check n > 0
|
|
check frame.masked
|
|
check frame.payload == "SUBSCRIBE t"
|
|
|
|
test "WebSocket decode rejects oversized frame length":
|
|
var raw = newString(10)
|
|
raw[0] = char(0x81) # FIN + text
|
|
raw[1] = char(0xFF) # MASK + 127
|
|
# 8-byte length = 2 GiB
|
|
raw[2] = char(0)
|
|
raw[3] = char(0)
|
|
raw[4] = char(0)
|
|
raw[5] = char(0)
|
|
raw[6] = char(0x80)
|
|
raw[7] = char(0)
|
|
raw[8] = char(0)
|
|
raw[9] = char(0)
|
|
let (_, n) = decodeFrame(raw)
|
|
check n < 0
|
|
|
|
test "disttxn prepare against refused port fails closed":
|
|
var tm = newDistTxnManager()
|
|
let txn = tm.beginTransaction("coord")
|
|
txn.addParticipant("n1", "127.0.0.1", 1)
|
|
check txn.prepare() == false
|
|
check txn.isAborted
|
|
|
|
test "compact leaves input files on disk for catalog apply":
|
|
let testDir = "/tmp/baradb_bugfix_compact_order"
|
|
removeDir(testDir)
|
|
var db = newLSMTree(testDir, 128)
|
|
defer:
|
|
db.close()
|
|
removeDir(testDir)
|
|
for round in 0 ..< L0CompactionTrigger:
|
|
db.put("r" & $round, cast[seq[byte]]("v" & $round))
|
|
db.flush()
|
|
var cs = newCompactionStrategy(testDir)
|
|
cs.rebuildFromLSM(db)
|
|
let cr = cs.compact(0)
|
|
check cr.outputTables.len == 1
|
|
check fileExists(cr.outputTables[0].path)
|
|
for t in cr.inputTables:
|
|
check fileExists(t.path)
|
|
|
|
test "SCRAM unknown user fails without leaking existence":
|
|
var am = newAuthManager()
|
|
am.registerScramUser("alice", "wonderland")
|
|
var msg = ""
|
|
try:
|
|
discard am.startScram("n,,n=eve,r=abcnonceabcnonceabcn")
|
|
except ValueError as e:
|
|
msg = e.msg
|
|
check msg == "Authentication failed"
|
|
|
|
test "SCRAM rejects mismatched channel binding":
|
|
var am = newAuthManager()
|
|
am.registerScramUser("alice", "wonderland")
|
|
let clientNonce = generateNonce()
|
|
let clientFirst = "n,,n=alice,r=" & clientNonce
|
|
let serverFirst = am.startScram(clientFirst)
|
|
var combinedNonce = ""
|
|
for part in serverFirst.split(","):
|
|
if part.startsWith("r="): combinedNonce = part[2..^1]
|
|
let (ok, err) = am.finishScram("c=AAAA,r=" & combinedNonce & ",p=AA")
|
|
check ok == false
|
|
check err == "e=channel-bindings-dont-match"
|
|
|
|
test "SCRAM expected cbind for gs2 n is biws":
|
|
check expectedChannelBinding("n") == "biws"
|
|
|
|
|
|
suite "Audit batch 4 — B-tree separator (H10) and NULL equality (L4)":
|
|
|
|
test "B-tree separators stay valid after interleaved insert/remove":
|
|
## Search routes with `key > sep → right`. After delete, removeRec used to
|
|
## copy the right child's first key into the separator (right-min), which
|
|
## breaks max(left) <= sep < min(right).
|
|
var rng = initRand(20260828)
|
|
var btree = newBTreeIndex[int, string](order = 5)
|
|
var tracker = initTable[int, seq[string]]()
|
|
for i in 0..<400:
|
|
let k = rng.rand(0..80)
|
|
if rng.rand(0..2) < 2:
|
|
let v = "v" & $i
|
|
btree.insert(k, v)
|
|
if k notin tracker: tracker[k] = @[]
|
|
tracker[k].add(v)
|
|
else:
|
|
if k in tracker and tracker[k].len > 0:
|
|
let v = tracker[k][0]
|
|
btree.remove(k, v)
|
|
tracker[k].del(0)
|
|
if tracker[k].len == 0: tracker.del(k)
|
|
if i mod 50 == 49:
|
|
check btree.checkInvariants().len == 0
|
|
for k, vals in tracker:
|
|
check btree.get(k).toHashSet == vals.toHashSet
|
|
|
|
test "B-tree sequential fill + prefix delete keeps separators":
|
|
var btree = newBTreeIndex[int, string](order = 4)
|
|
for i in 0..<60:
|
|
btree.insert(i, "v" & $i)
|
|
for i in 0..<30:
|
|
btree.remove(i, "v" & $i)
|
|
check btree.checkInvariants().len == 0
|
|
for i in 30..<60:
|
|
check btree.get(i) == @["v" & $i]
|
|
for i in 0..<30:
|
|
check btree.get(i).len == 0
|
|
|
|
test "NULL = NULL is unknown, not true":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, NULL)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
let eqNull = executeQuery(ctx, parse("SELECT id FROM users WHERE name = NULL"))
|
|
check eqNull.success
|
|
check eqNull.rows.len == 0
|
|
let isNull = executeQuery(ctx, parse("SELECT id FROM users WHERE name IS NULL"))
|
|
check isNull.success
|
|
check isNull.rows.len == 1
|
|
check valueToString(isNull.rows[0]["id"]) == "1"
|
|
|
|
test "NULL != value is unknown so WHERE excludes the row":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, NULL)"))
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (2, 'bob')"))
|
|
let r = executeQuery(ctx, parse("SELECT id FROM users WHERE name != 'bob'"))
|
|
check r.success
|
|
check r.rows.len == 0
|
|
|
|
test "SELECT NULL = NULL yields NULL, IS NULL is true":
|
|
var ctx = setupCtx()
|
|
defer: teardown(ctx)
|
|
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'a')"))
|
|
let eq = executeQuery(ctx, parse("SELECT (NULL = NULL) AS x FROM users"))
|
|
check eq.success
|
|
check eq.rows.len == 1
|
|
check valueToString(eq.rows[0]["x"]) == "\\N"
|
|
let isn = executeQuery(ctx, parse("SELECT (NULL IS NULL) AS x FROM users"))
|
|
check isn.success
|
|
check valueToString(isn.rows[0]["x"]) == "true"
|