feat: production blockers — JOINs, deadlock detection, TLS, parameterized queries

- JOIN execution: INNER/LEFT/RIGHT/FULL/CROSS with column disambiguation
- Deadlock detection: wait-for graph wired into TxnManager.write()
- TLS/SSL: OpenSSL via std/net for TCP wire protocol, auto self-signed certs
- Parameterized queries: ? placeholders with WireValue binding
- Wire protocol: mkQueryParams message support
- HTTP /query endpoint accepts JSON params array
- Nim client: query(sql, params) overload
- Tests: 262 passing (15 new)

All PLAN.md production blockers resolved.
This commit is contained in:
2026-05-06 16:42:53 +03:00
parent 856a07c030
commit f9f77b3a18
25 changed files with 3088 additions and 797 deletions
+75
View File
@@ -0,0 +1,75 @@
import std/unittest
import std/os
import std/strutils
import barabadb/core/types
import barabadb/query/executor as qexec
import barabadb/query/parser
import barabadb/query/ast
import barabadb/storage/lsm
proc execSql(ctx: qexec.ExecutionContext, sql: string): qexec.ExecResult =
qexec.executeQuery(ctx, parse(sql))
# ---------------------------------------------------------------------------
suite "JOIN execution":
var db: LSMTree
var ctx: qexec.ExecutionContext
setup:
db = newLSMTree("")
ctx = qexec.newExecutionContext(db)
discard execSql(ctx, "CREATE TABLE users (id INT, name TEXT)")
discard execSql(ctx, "CREATE TABLE orders (id INT, user_id INT, total REAL)")
discard execSql(ctx, "INSERT INTO users (id, name) VALUES (1, 'Alice')")
discard execSql(ctx, "INSERT INTO users (id, name) VALUES (2, 'Bob')")
discard execSql(ctx, "INSERT INTO orders (id, user_id, total) VALUES (10, 1, 99.5)")
discard execSql(ctx, "INSERT INTO orders (id, user_id, total) VALUES (20, 1, 23.0)")
discard execSql(ctx, "INSERT INTO orders (id, user_id, total) VALUES (30, 3, 150.0)")
teardown:
discard
test "INNER JOIN returns matching rows only":
let r = execSql(ctx, "SELECT * FROM users u JOIN orders o ON u.id = o.user_id")
check r.rows.len == 2
check r.rows[0]["name"] == "Alice"
check r.rows[0]["total"] == "99.5"
test "LEFT JOIN keeps unmatched left rows":
let r = execSql(ctx, "SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id")
check r.rows.len == 3
check r.rows[0]["name"] == "Alice"
check r.rows[1]["name"] == "Alice"
check r.rows[2]["name"] == "Bob"
check r.rows[2]["total"] == "" # NULL represented as empty string
test "RIGHT JOIN keeps unmatched right rows":
let r = execSql(ctx, "SELECT * FROM users u RIGHT JOIN orders o ON u.id = o.user_id")
check r.rows.len == 3
check r.rows[2]["id"] == "30"
check r.rows[2]["name"] == "" # NULL
check r.rows[2]["total"] == "150.0"
test "FULL JOIN keeps all rows":
let r = execSql(ctx, "SELECT * FROM users u FULL JOIN orders o ON u.id = o.user_id")
check r.rows.len == 4
test "CROSS JOIN cartesian product":
let r = execSql(ctx, "SELECT * FROM users u CROSS JOIN orders o")
check r.rows.len == 6
test "aliased column projection":
let r = execSql(ctx, "SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id")
check r.rows.len == 2
check r.rows[0]["name"] == "Alice"
check r.rows[0]["total"] == "99.5"
check "id" notin r.rows[0]
test "count after FULL JOIN":
let r = execSql(ctx, "SELECT COUNT(*) AS cnt FROM users u FULL JOIN orders o ON u.id = o.user_id")
check r.rows.len == 1
check r.rows[0]["cnt"] == "4"
test "count after CROSS JOIN":
let r = execSql(ctx, "SELECT COUNT(*) AS cnt FROM users u CROSS JOIN orders o")
check r.rows[0]["cnt"] == "6"
+378 -3
View File
@@ -8,6 +8,8 @@ import std/asyncdispatch
import barabadb/core/types
import barabadb/core/mvcc
import barabadb/core/deadlock
import barabadb/core/config
import barabadb/core/server
import barabadb/core/columnar
import barabadb/core/raft
import barabadb/core/sharding
@@ -31,6 +33,7 @@ import barabadb/client/fileops
import barabadb/fts/multilang as mlang
import barabadb/protocol/zerocopy
import barabadb/query/adaptive
import barabadb/query/executor as qexec
import barabadb/core/disttxn
import barabadb/vector/engine as vengine
import barabadb/graph/cypher
@@ -409,6 +412,44 @@ suite "Deadlock Detection":
dd.removeTxn(2)
check not dd.hasDeadlock()
suite "MVCC Deadlock Detection":
test "TxnManager detects and breaks deadlock":
var tm = newTxnManager()
var t1 = tm.beginTxn()
var t2 = tm.beginTxn()
# t1 writes key "a"
check tm.write(t1, "a", @[1'u8])
# t2 writes key "b"
check tm.write(t2, "b", @[2'u8])
# t2 tries to write "a" — conflicts with t1 (active), adds wait edge t2->t1
check not tm.write(t2, "a", @[3'u8])
# t1 tries to write "b" — conflicts with t2 (active), adds wait edge t1->t2
# This creates a cycle: t1->t2->t1
check not tm.write(t1, "b", @[4'u8])
# One of the transactions should have been aborted as victim
let t1Active = t1.state == tsActive
let t2Active = t2.state == tsActive
# At least one victim must be aborted
check (not t1Active) or (not t2Active)
# The survivor should be able to commit
if t1Active:
check tm.commit(t1)
if t2Active:
check tm.commit(t2)
test "No false deadlock on sequential writes":
var tm = newTxnManager()
var t1 = tm.beginTxn()
# t1 writes key "a"
check tm.write(t1, "a", @[1'u8])
# t1 commits
check tm.commit(t1)
# t2 begins after t1 committed
var t2 = tm.beginTxn()
# t2 writes same key — no active conflict
check tm.write(t2, "a", @[2'u8])
check tm.commit(t2)
suite "Wire Protocol":
test "Value serialization roundtrip":
var buf: seq[byte] = @[]
@@ -1934,9 +1975,13 @@ suite "TLS/SSL":
check info.subject.len > 0
check info.isSelfSigned # subject == issuer
test "TLS socket connect — missing cert":
var sock = newTLSSocket(newTLSConfig("nonexistent.pem", "nonexistent.key"))
check not sock.connect("localhost", 443)
test "TLS context creation with missing cert raises":
var raised = false
try:
discard newTLSContext(newTLSConfig("nonexistent.pem", "nonexistent.key"))
except IOError:
raised = true
check raised
test "Generate self-signed cert":
let (certPath, keyPath) = generateSelfSignedCert("/tmp/baradb_test_tls", "test.local")
@@ -1944,3 +1989,333 @@ suite "TLS/SSL":
if certPath.len > 0:
check fileExists(certPath)
check fileExists(keyPath)
# Should be able to create TLS context from generated cert
let ctx = newTLSContext(newTLSConfig(certPath, keyPath))
check ctx != nil
test "Server with TLS config":
var cfg = defaultConfig()
cfg.tlsEnabled = true
let (certPath, keyPath) = generateSelfSignedCert("/tmp/baradb_test_tls2", "test.local")
if certPath.len > 0:
cfg.certFile = certPath
cfg.keyFile = keyPath
var srv = newServer(cfg)
check srv != nil
check srv.tls != nil
suite "Triggers":
test "Parse CREATE TRIGGER":
let ast = parse("CREATE TRIGGER log_insert BEFORE INSERT ON users AS INSERT INTO audit_log VALUES ('insert', 'users')")
check ast.stmts.len == 1
check ast.stmts[0].kind == nkCreateTrigger
check ast.stmts[0].trigName == "log_insert"
check ast.stmts[0].trigTable == "users"
check ast.stmts[0].trigTiming == "before"
check ast.stmts[0].trigEvent == "INSERT"
check ast.stmts[0].trigAction.strVal.contains("INSERT")
check ast.stmts[0].trigAction.strVal.contains("audit_log")
test "Parse CREATE TRIGGER AFTER UPDATE":
let ast = parse("CREATE TRIGGER audit_update AFTER UPDATE ON orders AS INSERT INTO audit VALUES ('updated')")
check ast.stmts[0].kind == nkCreateTrigger
check ast.stmts[0].trigTiming == "after"
check ast.stmts[0].trigEvent == "UPDATE"
test "Parse CREATE TRIGGER INSTEAD OF DELETE":
let ast = parse("CREATE TRIGGER soft_delete INSTEAD OF DELETE ON users AS UPDATE users SET deleted = true WHERE id = OLD.id")
check ast.stmts[0].kind == nkCreateTrigger
check ast.stmts[0].trigTiming == "instead of"
check ast.stmts[0].trigEvent == "DELETE"
test "Parse DROP TRIGGER":
let ast = parse("DROP TRIGGER log_insert")
check ast.stmts.len == 1
check ast.stmts[0].kind == nkDropTrigger
check ast.stmts[0].trigDropName == "log_insert"
check ast.stmts[0].trigDropIfExists == false
test "Parse DROP TRIGGER IF EXISTS":
let ast = parse("DROP TRIGGER IF EXISTS old_trigger")
check ast.stmts[0].kind == nkDropTrigger
check ast.stmts[0].trigDropName == "old_trigger"
check ast.stmts[0].trigDropIfExists == true
suite "Row-Level Security":
test "Parse CREATE USER":
let ast = parse("CREATE USER admin WITH PASSWORD 'secret' SUPERUSER")
check ast.stmts.len == 1
check ast.stmts[0].kind == nkCreateUser
check ast.stmts[0].cuName == "admin"
check ast.stmts[0].cuPassword == "secret"
check ast.stmts[0].cuSuperuser == true
test "Parse CREATE USER without superuser":
let ast = parse("CREATE USER reader WITH PASSWORD 'reader123'")
check ast.stmts[0].kind == nkCreateUser
check ast.stmts[0].cuName == "reader"
check ast.stmts[0].cuPassword == "reader123"
check ast.stmts[0].cuSuperuser == false
test "Parse DROP USER":
let ast = parse("DROP USER admin")
check ast.stmts[0].kind == nkDropUser
check ast.stmts[0].duName == "admin"
test "Parse CREATE POLICY":
let ast = parse("CREATE POLICY user_isolation ON accounts FOR SELECT USING (user_id = current_user)")
check ast.stmts[0].kind == nkCreatePolicy
check ast.stmts[0].cpName == "user_isolation"
check ast.stmts[0].cpTable == "accounts"
check ast.stmts[0].cpCommand == "SELECT"
test "Parse CREATE POLICY with WITH CHECK":
let ast = parse("CREATE POLICY insert_check ON accounts FOR INSERT WITH CHECK (amount > 0)")
check ast.stmts[0].kind == nkCreatePolicy
check ast.stmts[0].cpCommand == "INSERT"
test "Parse DROP POLICY":
let ast = parse("DROP POLICY user_isolation ON accounts")
check ast.stmts[0].kind == nkDropPolicy
check ast.stmts[0].dpName == "user_isolation"
check ast.stmts[0].dpTable == "accounts"
test "Parse GRANT":
let ast = parse("GRANT SELECT ON accounts TO reader")
check ast.stmts[0].kind == nkGrant
check ast.stmts[0].grPrivilege == "SELECT"
check ast.stmts[0].grTable == "accounts"
check ast.stmts[0].grGrantee == "reader"
test "Parse REVOKE":
let ast = parse("REVOKE INSERT ON accounts FROM reader")
check ast.stmts[0].kind == nkRevoke
check ast.stmts[0].rvPrivilege == "INSERT"
check ast.stmts[0].rvTable == "accounts"
check ast.stmts[0].rvGrantee == "reader"
test "Parse ENABLE ROW LEVEL SECURITY":
let ast = parse("ALTER TABLE accounts ENABLE ROW LEVEL SECURITY")
check ast.stmts[0].kind == nkEnableRLS
check ast.stmts[0].erlsTable == "accounts"
test "Parse DISABLE ROW LEVEL SECURITY":
let ast = parse("ALTER TABLE accounts DISABLE ROW LEVEL SECURITY")
check ast.stmts[0].kind == nkDisableRLS
check ast.stmts[0].drlsTable == "accounts"
test "RLS filter on SELECT":
var db = newLSMTree("")
var ctx = qexec.newExecutionContext(db)
# Create table and insert data
discard qexec.executeQuery(ctx, parse("CREATE TABLE docs (id INTEGER, owner TEXT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO docs (id, owner) VALUES (1, 'alice'), (2, 'bob')"))
# Create user and policy
ctx.currentUser = "alice"
ctx.users["alice"] = qexec.UserDef(name: "alice", passwordHash: "", isSuperuser: false, roles: @[])
ctx.policies["docs"] = @[
qexec.PolicyDef(name: "owner_only", tableName: "docs", command: "SELECT",
usingExpr: Node(kind: nkBinOp, binOp: bkEq,
binLeft: Node(kind: nkIdent, identName: "owner"),
binRight: Node(kind: nkStringLit, strVal: "alice")),
withCheckExpr: nil)
]
# Query should only return alice's row
let res = qexec.executeQuery(ctx, parse("SELECT id, owner FROM docs"))
check res.success
check res.rows.len == 1
check res.rows[0]["owner"] == "alice"
test "RLS superuser bypass":
var db = newLSMTree("")
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE docs (id INTEGER, owner TEXT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO docs (id, owner) VALUES (1, 'alice')"))
ctx.currentUser = "admin"
ctx.users["admin"] = qexec.UserDef(name: "admin", passwordHash: "", isSuperuser: true, roles: @[])
ctx.policies["docs"] = @[
qexec.PolicyDef(name: "owner_only", tableName: "docs", command: "SELECT",
usingExpr: Node(kind: nkBinOp, binOp: bkEq,
binLeft: Node(kind: nkIdent, identName: "owner"),
binRight: Node(kind: nkStringLit, strVal: "alice")),
withCheckExpr: nil)
]
let res = qexec.executeQuery(ctx, parse("SELECT id, owner FROM docs"))
check res.success
check res.rows.len == 1 # superuser sees all (only 1 row exists)
suite "UTF-8 Support":
test "Tokenize UTF-8 identifiers":
let tokens = lex.tokenize("SELECT имя FROM потребители")
check tokens[1].kind == tkIdent
check tokens[1].value == "имя"
check tokens[3].kind == tkIdent
check tokens[3].value == "потребители"
test "Parse UTF-8 table and column names":
let ast = parse("SELECT имя, възраст FROM потребители WHERE град = 'София'")
check ast.stmts[0].kind == nkSelect
check ast.stmts[0].selFrom.fromTable == "потребители"
check ast.stmts[0].selResult[0].identName == "имя"
check ast.stmts[0].selWhere.whereExpr.binRight.strVal == "София"
test "Execute query with UTF-8 data":
var db = newLSMTree("")
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE потребители (имя TEXT, град TEXT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO потребители (имя, град) VALUES ('Иван', 'София'), ('Мария', 'Пловдив')"))
let res = qexec.executeQuery(ctx, parse("SELECT имя, град FROM потребители WHERE град = 'София'"))
check res.success
check res.rows.len == 1
check res.rows[0]["имя"] == "Иван"
check res.rows[0]["град"] == "София"
suite "Enhanced Migrations":
test "Parse CREATE MIGRATION with UP/DOWN":
let ast = parse("CREATE MIGRATION add_users { UP: CREATE TABLE users (id INTEGER PRIMARY KEY); DOWN: DROP TABLE users; }")
check ast.stmts.len == 1
check ast.stmts[0].kind == nkCreateMigration
check ast.stmts[0].cmName == "add_users"
check ast.stmts[0].cmBody.contains("CREATE TABLE users")
check ast.stmts[0].cmDownBody.contains("DROP TABLE users")
test "Parse MIGRATION STATUS":
let ast = parse("MIGRATION STATUS")
check ast.stmts[0].kind == nkMigrationStatus
test "Parse MIGRATION UP":
let ast = parse("MIGRATION UP")
check ast.stmts[0].kind == nkMigrationUp
check ast.stmts[0].muCount == 0
test "Parse MIGRATION UP 5":
let ast = parse("MIGRATION UP 5")
check ast.stmts[0].kind == nkMigrationUp
check ast.stmts[0].muCount == 5
test "Parse MIGRATION DOWN":
let ast = parse("MIGRATION DOWN")
check ast.stmts[0].kind == nkMigrationDown
check ast.stmts[0].mdCount == 1
test "Parse MIGRATION DOWN 3":
let ast = parse("MIGRATION DOWN 3")
check ast.stmts[0].kind == nkMigrationDown
check ast.stmts[0].mdCount == 3
test "Parse MIGRATION DRYRUN":
let ast = parse("MIGRATION DRYRUN add_users")
check ast.stmts[0].kind == nkMigrationDryRun
check ast.stmts[0].mdrName == "add_users"
test "Create and apply migration with checksum":
var db = newLSMTree("")
var ctx = qexec.newExecutionContext(db)
# Create migration
let createRes = qexec.executeQuery(ctx, parse("CREATE MIGRATION add_users { UP: CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); DOWN: DROP TABLE users; }"))
check createRes.success
check createRes.message.contains("checksum")
# Apply migration
let applyRes = qexec.executeQuery(ctx, parse("APPLY MIGRATION add_users"))
check applyRes.success
check applyRes.message.contains("ms")
# Check table exists
let tableRes = qexec.executeQuery(ctx, parse("SELECT name FROM users"))
check tableRes.success # table exists (empty result is OK)
# Re-apply should be idempotent
let reapplyRes = qexec.executeQuery(ctx, parse("APPLY MIGRATION add_users"))
check reapplyRes.success
check reapplyRes.message.contains("already applied")
test "Migration STATUS shows applied migrations":
var db = newLSMTree("")
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE MIGRATION m1 { UP: CREATE TABLE t1 (id INTEGER); }"))
discard qexec.executeQuery(ctx, parse("CREATE MIGRATION m2 { UP: CREATE TABLE t2 (id INTEGER); }"))
discard qexec.executeQuery(ctx, parse("APPLY MIGRATION m1"))
let statusRes = qexec.executeQuery(ctx, parse("MIGRATION STATUS"))
check statusRes.success
check statusRes.rows.len == 2
check statusRes.rows[0]["status"] == "applied"
check statusRes.rows[1]["status"] == "pending"
test "Migration UP applies all pending":
var db = newLSMTree("")
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE MIGRATION m1 { UP: CREATE TABLE t1 (id INTEGER); }"))
discard qexec.executeQuery(ctx, parse("CREATE MIGRATION m2 { UP: CREATE TABLE t2 (id INTEGER); }"))
let upRes = qexec.executeQuery(ctx, parse("MIGRATION UP"))
check upRes.success
check upRes.message.contains("Applied 2 migrations")
test "Migration DOWN rollback":
var db = newLSMTree("")
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE MIGRATION add_t { UP: CREATE TABLE t (id INTEGER); DOWN: DROP TABLE t; }"))
discard qexec.executeQuery(ctx, parse("APPLY MIGRATION add_t"))
let downRes = qexec.executeQuery(ctx, parse("MIGRATION DOWN"))
check downRes.success
check downRes.message.contains("Rolled back 1 migrations")
# After rollback, table should be gone (check by listing tables)
let tableRes = qexec.executeQuery(ctx, parse("SELECT name FROM __tables WHERE name = 't'"))
check tableRes.success
check tableRes.rows.len == 0 # table does not exist
test "Migration DRYRUN":
var db = newLSMTree("")
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE MIGRATION add_t { UP: CREATE TABLE t (id INTEGER); CREATE INDEX idx ON t(id); DOWN: DROP TABLE t; }"))
let dryRes = qexec.executeQuery(ctx, parse("MIGRATION DRYRUN add_t"))
check dryRes.success
check dryRes.message.contains("DRY RUN")
check dryRes.message.contains("Statements: 2")
check dryRes.message.contains("DOWN script: yes")
suite "Parameterized queries":
var db: LSMTree
var ctx: qexec.ExecutionContext
setup:
db = newLSMTree("")
ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE users (id INT, name TEXT, age INT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25)"))
test "SELECT with placeholder params":
let sql = "SELECT * FROM users WHERE id = ?"
let tokens = lex.tokenize(sql)
let ast = parse(tokens)
let params = @[WireValue(kind: fkInt64, int64Val: 1)]
let r = qexec.executeQuery(ctx, ast, params)
check r.success
check r.rows.len == 1
check r.rows[0]["name"] == "Alice"
test "INSERT with placeholder params":
let sql = "INSERT INTO users (id, name, age) VALUES (?, ?, ?)"
let tokens = lex.tokenize(sql)
let ast = parse(tokens)
let params = @[
WireValue(kind: fkInt64, int64Val: 3),
WireValue(kind: fkString, strVal: "Charlie"),
WireValue(kind: fkInt64, int64Val: 35)
]
let r = qexec.executeQuery(ctx, ast, params)
check r.success
let selectR = qexec.executeQuery(ctx, parse("SELECT * FROM users WHERE id = 3"))
check selectR.rows.len == 1
check selectR.rows[0]["name"] == "Charlie"
test "SELECT with multiple placeholders":
let sql = "SELECT * FROM users WHERE age > ? AND name = ?"
let tokens = lex.tokenize(sql)
let ast = parse(tokens)
let params = @[WireValue(kind: fkInt64, int64Val: 25), WireValue(kind: fkString, strVal: "Alice")]
let r = qexec.executeQuery(ctx, ast, params)
check r.success
check r.rows.len == 1
check r.rows[0]["name"] == "Alice"
# JOIN tests
include "join_tests"