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:
+378
-3
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user