fix(adapter,executor,tests): harden nimforum adapter and fix transaction/aggregate bugs
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

Adapter (baradb_sqlite.nim):
- getRow now returns @[] for missing rows (db_sqlite compat)
- insertID/nextId handle NULL (\N) values correctly
- preprocessQuery replaces SQLite-specific syntax:
  TIMESTAMP->DATETIME, DATETIME('now')->current timestamp, INET->STRING
- add dbBegin/dbCommit/dbRollback for transaction support

Executor:
- fix COMMIT treating empty values as DELETE (rows with only PK were lost)
- fix MIN/MAX aggregates including NULL (\N) which skewed results
- add numeric comparison for irNeq

Tests:
- stress_test: shared DB, 16 workers, correctness scan after run
- nimforum_smoke_test: 10 tests including schema, transactions, iterators
This commit is contained in:
2026-05-19 20:43:00 +03:00
parent d3b77174da
commit 14596c9584
7 changed files with 453 additions and 102 deletions
+1
View File
@@ -46,3 +46,4 @@ src/barabadb/storage/compaction
src/barabadb/query/executor src/barabadb/query/executor
tests/join_tests tests/join_tests
*.tar.gz *.tar.gz
tests/nimforum_smoke_test
+32 -5
View File
@@ -1,6 +1,6 @@
## BaraDB adapter mimicking db_sqlite API for NimForum ## BaraDB adapter mimicking db_sqlite API for NimForum
import std/strutils, std/tables, std/sequtils, std/parseutils import std/strutils, std/parseutils, std/times
import ../../clients/nim/src/baradb/client as baradb_client import ../../clients/nim/src/baradb/client as baradb_client
type type
@@ -24,6 +24,17 @@ proc dbQuote*(s: string): string =
else: add(result, c) else: add(result, c)
add(result, '\'') add(result, '\'')
proc preprocessQuery(q: string): string =
## Replace SQLite-specific expressions with BaraDB-compatible equivalents.
result = q
let nowStr = format(now(), "yyyy-MM-dd HH:mm:ss")
result = result.replace("TIMESTAMP", "DATETIME")
result = result.replace("timestamp", "DATETIME")
result = result.replace("DATETIME('now')", "'" & nowStr & "'")
result = result.replace("datetime('now')", "'" & nowStr & "'")
result = result.replace("INET", "STRING")
result = result.replace("inet", "STRING")
proc dbFormat*(formatstr: SqlQuery, args: varargs[string]): string = proc dbFormat*(formatstr: SqlQuery, args: varargs[string]): string =
var res = "" var res = ""
var a = 0 var a = 0
@@ -35,7 +46,7 @@ proc dbFormat*(formatstr: SqlQuery, args: varargs[string]): string =
inc(a) inc(a)
else: else:
add(res, c) add(res, c)
res result = preprocessQuery(res)
proc toClient(db: DbConn): SyncClient = proc toClient(db: DbConn): SyncClient =
cast[SyncClient](db) cast[SyncClient](db)
@@ -82,7 +93,7 @@ proc getRow*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): seq[strin
if qr.rows.len > 0: if qr.rows.len > 0:
return qr.rows[0] return qr.rows[0]
else: else:
return newSeq[string](qr.columns.len) return @[]
proc getAllRows*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): seq[seq[string]] = proc getAllRows*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): seq[seq[string]] =
let client = toClient(db) let client = toClient(db)
@@ -119,8 +130,10 @@ proc insertID*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): int64 =
if tableName.len > 0: if tableName.len > 0:
let idQr = client.query("SELECT max(id) FROM " & tableName) let idQr = client.query("SELECT max(id) FROM " & tableName)
if idQr.rows.len > 0 and idQr.rows[0].len > 0: if idQr.rows.len > 0 and idQr.rows[0].len > 0:
let val = idQr.rows[0][0]
if val.len > 0 and val != "\\N":
try: try:
return parseInt(idQr.rows[0][0]).int64 return parseInt(val).int64
except: except:
discard discard
return -1 return -1
@@ -135,8 +148,10 @@ proc nextId*(db: DbConn, tableName: string): int64 =
let client = toClient(db) let client = toClient(db)
let qr = client.query("SELECT max(id) FROM " & tableName) let qr = client.query("SELECT max(id) FROM " & tableName)
if qr.rows.len > 0 and qr.rows[0].len > 0: if qr.rows.len > 0 and qr.rows[0].len > 0:
let val = qr.rows[0][0]
if val.len > 0 and val != "\\N":
try: try:
let current = parseInt(qr.rows[0][0]) let current = parseInt(val)
return current.int64 + 1 return current.int64 + 1
except: except:
return 1 return 1
@@ -147,3 +162,15 @@ proc execAffectedRows*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]):
let q = dbFormat(query, args) let q = dbFormat(query, args)
let qr = client.query(q) let qr = client.query(q)
return qr.affectedRows.int64 return qr.affectedRows.int64
proc dbBegin*(db: DbConn) =
let client = toClient(db)
discard client.query("BEGIN")
proc dbCommit*(db: DbConn) =
let client = toClient(db)
discard client.query("COMMIT")
proc dbRollback*(db: DbConn) =
let client = toClient(db)
discard client.query("ROLLBACK")
+3
View File
@@ -428,14 +428,17 @@ type
of nkInExpr: of nkInExpr:
inLeft*: Node inLeft*: Node
inRight*: Node inRight*: Node
inNegated*: bool
of nkBetweenExpr: of nkBetweenExpr:
betweenExpr*: Node betweenExpr*: Node
betweenLow*: Node betweenLow*: Node
betweenHigh*: Node betweenHigh*: Node
betweenNegated*: bool
of nkLikeExpr: of nkLikeExpr:
likeExpr*: Node likeExpr*: Node
likePattern*: Node likePattern*: Node
likeCaseInsensitive*: bool likeCaseInsensitive*: bool
likeNegated*: bool
of nkIsExpr: of nkIsExpr:
isExpr*: Node isExpr*: Node
isType*: string isType*: string
+94 -26
View File
@@ -252,6 +252,8 @@ proc exprToSql(node: Node): string =
return node.funcName & "()" return node.funcName & "()"
of nkUnaryOp: of nkUnaryOp:
return $node.unOp & " " & exprToSql(node.unOperand) return $node.unOp & " " & exprToSql(node.unOperand)
of nkPath:
return node.pathParts.join(".")
else: else:
return $node.kind return $node.kind
@@ -956,7 +958,12 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
if parseFloat(left) == parseFloat(right): return "true" if parseFloat(left) == parseFloat(right): return "true"
except: discard except: discard
return "false" return "false"
of irNeq: return if left != right: "true" else: "false" of irNeq:
if left != right: return "true"
# Try numeric comparison
try:
return if parseFloat(left) != parseFloat(right): "true" else: "false"
except: return "false"
of irLt: of irLt:
try: try:
return if parseFloat(left) < parseFloat(right): "true" else: "false" return if parseFloat(left) < parseFloat(right): "true" else: "false"
@@ -2374,7 +2381,25 @@ proc lowerExpr*(node: Node): IRExpr =
result.binOp = if node.likeCaseInsensitive: irILike else: irLike result.binOp = if node.likeCaseInsensitive: irILike else: irLike
result.binLeft = lowerExpr(node.likeExpr) result.binLeft = lowerExpr(node.likeExpr)
result.binRight = lowerExpr(node.likePattern) result.binRight = lowerExpr(node.likePattern)
if node.likeNegated:
let wrapped = result
result = IRExpr(kind: irekUnary, valueKind: vkBool, unOp: irNot, unExpr: wrapped)
of nkBetweenExpr: of nkBetweenExpr:
if node.betweenNegated:
# NOT BETWEEN => NOT (expr >= low AND expr <= high)
result = IRExpr(kind: irekBinary, valueKind: vkBool)
result.binOp = irOr
let leftCmp = IRExpr(kind: irekBinary, valueKind: vkBool)
leftCmp.binOp = irLt
leftCmp.binLeft = lowerExpr(node.betweenExpr)
leftCmp.binRight = lowerExpr(node.betweenLow)
let rightCmp = IRExpr(kind: irekBinary, valueKind: vkBool)
rightCmp.binOp = irGt
rightCmp.binLeft = lowerExpr(node.betweenExpr)
rightCmp.binRight = lowerExpr(node.betweenHigh)
result.binLeft = leftCmp
result.binRight = rightCmp
else:
result = IRExpr(kind: irekBinary, valueKind: vkBool) result = IRExpr(kind: irekBinary, valueKind: vkBool)
result.binOp = irAnd result.binOp = irAnd
let leftCmp = IRExpr(kind: irekBinary, valueKind: vkBool) let leftCmp = IRExpr(kind: irekBinary, valueKind: vkBool)
@@ -2389,6 +2414,20 @@ proc lowerExpr*(node: Node): IRExpr =
result.binRight = rightCmp result.binRight = rightCmp
of nkInExpr: of nkInExpr:
if node.inRight.kind == nkArrayLit: if node.inRight.kind == nkArrayLit:
if node.inNegated:
# NOT IN (list) => AND of != comparisons
result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkBool, boolVal: true))
for elem in node.inRight.arrayElems:
let neqCmp = IRExpr(kind: irekBinary)
neqCmp.binOp = irNeq
neqCmp.binLeft = lowerExpr(node.inLeft)
neqCmp.binRight = lowerExpr(elem)
let andNode = IRExpr(kind: irekBinary)
andNode.binOp = irAnd
andNode.binLeft = result
andNode.binRight = neqCmp
result = andNode
else:
result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkBool, boolVal: false)) result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkBool, boolVal: false))
for elem in node.inRight.arrayElems: for elem in node.inRight.arrayElems:
let eqCmp = IRExpr(kind: irekBinary) let eqCmp = IRExpr(kind: irekBinary)
@@ -2402,13 +2441,13 @@ proc lowerExpr*(node: Node): IRExpr =
result = orNode result = orNode
elif node.inRight.kind == nkSubquery: elif node.inRight.kind == nkSubquery:
result = IRExpr(kind: irekBinary) result = IRExpr(kind: irekBinary)
result.binOp = irIn result.binOp = if node.inNegated: irNotIn else: irIn
result.binLeft = lowerExpr(node.inLeft) result.binLeft = lowerExpr(node.inLeft)
result.binRight = IRExpr(kind: irekSubquery) result.binRight = IRExpr(kind: irekSubquery)
result.binRight.subqueryPlan = lowerSelect(node.inRight.subQuery) result.binRight.subqueryPlan = lowerSelect(node.inRight.subQuery)
else: else:
result = IRExpr(kind: irekBinary) result = IRExpr(kind: irekBinary)
result.binOp = irEq result.binOp = if node.inNegated: irNeq else: irEq
result.binLeft = lowerExpr(node.inLeft) result.binLeft = lowerExpr(node.inLeft)
result.binRight = lowerExpr(node.inRight) result.binRight = lowerExpr(node.inRight)
of nkExists: of nkExists:
@@ -2900,12 +2939,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var minVal = "" var minVal = ""
for row in filteredRows: for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row, ctx) let v = evalExpr(expr.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if minVal == "" or cmpMin(v, minVal): minVal = v if minVal == "" or cmpMin(v, minVal): minVal = v
newRow[alias] = minVal newRow[alias] = minVal
of irMax: of irMax:
var maxVal = "" var maxVal = ""
for row in filteredRows: for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row, ctx) let v = evalExpr(expr.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if maxVal == "" or cmpMax(v, maxVal): maxVal = v if maxVal == "" or cmpMax(v, maxVal): maxVal = v
newRow[alias] = maxVal newRow[alias] = maxVal
of irArrayAgg: of irArrayAgg:
@@ -2964,20 +3005,23 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
of irpkSort: of irpkSort:
var sourceRows = executePlan(ctx, plan.sortSource) var sourceRows = executePlan(ctx, plan.sortSource)
if plan.sortExprs.len == 0: return sourceRows if plan.sortExprs.len == 0: return sourceRows
let sortExpr = plan.sortExprs[0]
let ascending = if plan.sortDirs.len > 0: plan.sortDirs[0] else: true
proc sortCmp(a, b: Row): int = proc sortCmp(a, b: Row): int =
for i, sortExpr in plan.sortExprs:
let ascending = if i < plan.sortDirs.len: plan.sortDirs[i] else: true
let va = evalExpr(sortExpr, a, ctx) let va = evalExpr(sortExpr, a, ctx)
let vb = evalExpr(sortExpr, b, ctx) let vb = evalExpr(sortExpr, b, ctx)
var cmpRes = 0
try: try:
let fa = parseFloat(va) let fa = parseFloat(va)
let fb = parseFloat(vb) let fb = parseFloat(vb)
if fa < fb: return -1 if fa < fb: cmpRes = -1
if fa > fb: return 1 elif fa > fb: cmpRes = 1
return 0
except: except:
return cmp(va, vb) cmpRes = cmp(va, vb)
sourceRows.sort(sortCmp, if ascending: Ascending else: Descending) if not ascending: cmpRes = -cmpRes
if cmpRes != 0: return cmpRes
return 0
sourceRows.sort(sortCmp, Ascending)
return sourceRows return sourceRows
of irpkLimit: of irpkLimit:
@@ -3075,12 +3119,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var minVal = "" var minVal = ""
for row in filteredRows: for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row, ctx) let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if minVal == "" or cmpMin(v, minVal): minVal = v if minVal == "" or cmpMin(v, minVal): minVal = v
aggRow[aggKey] = minVal aggRow[aggKey] = minVal
of irMax: of irMax:
var maxVal = "" var maxVal = ""
for row in filteredRows: for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row, ctx) let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if maxVal == "" or cmpMax(v, maxVal): maxVal = v if maxVal == "" or cmpMax(v, maxVal): maxVal = v
aggRow[aggKey] = maxVal aggRow[aggKey] = maxVal
of irArrayAgg: of irArrayAgg:
@@ -3107,7 +3153,21 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
let leftRows = executePlan(ctx, plan.joinLeft) let leftRows = executePlan(ctx, plan.joinLeft)
result = @[] result = @[]
proc mergeRow(left, right: Row, leftAlias, rightAlias: string): Row = proc getScanTable(p: IRPlan): string =
if p == nil: return ""
case p.kind
of irpkScan: p.scanTable
of irpkJoin: getScanTable(p.joinRight)
else: ""
proc getLeftmostScanTable(p: IRPlan): string =
if p == nil: return ""
case p.kind
of irpkScan: p.scanTable
of irpkJoin: getLeftmostScanTable(p.joinLeft)
else: ""
proc mergeRow(left, right: Row, leftAlias, rightAlias, leftTable, rightTable: string): Row =
result = initTable[string, string]() result = initTable[string, string]()
for k, v in left: for k, v in left:
if not k.startsWith("$"): if not k.startsWith("$"):
@@ -3115,17 +3175,20 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
for k, v in right: for k, v in right:
if not k.startsWith("$") and k notin result: if not k.startsWith("$") and k notin result:
result[k] = v result[k] = v
if leftAlias.len > 0: let lq = if leftAlias.len > 0: leftAlias elif leftTable.len > 0: leftTable else: ""
if lq.len > 0:
for k, v in left: for k, v in left:
if not k.startsWith("$"): if not k.startsWith("$"):
result[leftAlias & "." & k] = v result[lq & "." & k] = v
if rightAlias.len > 0: let rq = if rightAlias.len > 0: rightAlias elif rightTable.len > 0: rightTable else: ""
if rq.len > 0:
for k, v in right: for k, v in right:
if not k.startsWith("$"): if not k.startsWith("$"):
result[rightAlias & "." & k] = v result[rq & "." & k] = v
let leftAlias = if plan.joinLeft != nil and plan.joinLeft.kind == irpkScan: let leftAlias = if plan.joinLeft != nil and plan.joinLeft.kind == irpkScan:
plan.joinLeft.scanAlias else: "" plan.joinLeft.scanAlias else: ""
let leftTable = getLeftmostScanTable(plan.joinLeft)
# LATERAL JOIN: for each left row, scan right, merge, then filter/sort/limit # LATERAL JOIN: for each left row, scan right, merge, then filter/sort/limit
if plan.joinLateral: if plan.joinLateral:
@@ -3169,7 +3232,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
# Merge, filter # Merge, filter
var mergedRows: seq[Row] var mergedRows: seq[Row]
for r in rawRightRows: for r in rawRightRows:
let merged = mergeRow(l, r, leftAlias, rightAlias) let merged = mergeRow(l, r, leftAlias, rightAlias, leftTable, getScanTable(plan.joinRight))
if rightFilter != nil and evalExpr(rightFilter, merged, ctx) != "true": if rightFilter != nil and evalExpr(rightFilter, merged, ctx) != "true":
continue continue
if plan.joinCond != nil and evalExpr(plan.joinCond, merged, ctx) != "true": if plan.joinCond != nil and evalExpr(plan.joinCond, merged, ctx) != "true":
@@ -3245,11 +3308,12 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
let rightAlias = if plan.joinRight != nil and plan.joinRight.kind == irpkScan: let rightAlias = if plan.joinRight != nil and plan.joinRight.kind == irpkScan:
plan.joinRight.scanAlias else: "" plan.joinRight.scanAlias else: ""
let rightTable = getScanTable(plan.joinRight)
if plan.joinKind == irjkCross: if plan.joinKind == irjkCross:
for l in leftRows: for l in leftRows:
for r in rightRows: for r in rightRows:
result.add(mergeRow(l, r, leftAlias, rightAlias)) result.add(mergeRow(l, r, leftAlias, rightAlias, leftTable, rightTable))
return result return result
case plan.joinStrategy case plan.joinStrategy
@@ -3296,9 +3360,9 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
matchedProbe[i] = true matchedProbe[i] = true
for brow in hashTable[key]: for brow in hashTable[key]:
if buildIsLeft: if buildIsLeft:
result.add(mergeRow(brow, prow, leftAlias, rightAlias)) result.add(mergeRow(brow, prow, leftAlias, rightAlias, leftTable, rightTable))
else: else:
result.add(mergeRow(prow, brow, leftAlias, rightAlias)) result.add(mergeRow(prow, brow, leftAlias, rightAlias, leftTable, rightTable))
# LEFT / RIGHT / FULL padding for non-matching probe rows # LEFT / RIGHT / FULL padding for non-matching probe rows
if plan.joinKind == irjkLeft or plan.joinKind == irjkFull or plan.joinKind == irjkRight: if plan.joinKind == irjkLeft or plan.joinKind == irjkFull or plan.joinKind == irjkRight:
@@ -3414,9 +3478,9 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
for entry in entries: for entry in entries:
let parsed = parseRowData(entry.rowValue) let parsed = parseRowData(entry.rowValue)
if outerIsLeft: if outerIsLeft:
result.add(mergeRow(orow, parsed, leftAlias, rightAlias)) result.add(mergeRow(orow, parsed, leftAlias, rightAlias, leftTable, rightTable))
else: else:
result.add(mergeRow(parsed, orow, leftAlias, rightAlias)) result.add(mergeRow(parsed, orow, leftAlias, rightAlias, leftTable, rightTable))
elif plan.joinKind == irjkLeft or plan.joinKind == irjkRight or plan.joinKind == irjkFull: elif plan.joinKind == irjkLeft or plan.joinKind == irjkRight or plan.joinKind == irjkFull:
matchedOuter[i] = false matchedOuter[i] = false
@@ -3455,7 +3519,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
for l in leftRows: for l in leftRows:
var matched = false var matched = false
for r in rightRows: for r in rightRows:
let merged = mergeRow(l, r, leftAlias, rightAlias) let merged = mergeRow(l, r, leftAlias, rightAlias, leftTable, rightTable)
if plan.joinCond == nil or evalExpr(plan.joinCond, merged, ctx) == "true": if plan.joinCond == nil or evalExpr(plan.joinCond, merged, ctx) == "true":
result.add(merged) result.add(merged)
matched = true matched = true
@@ -3479,7 +3543,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
for r in rightRows: for r in rightRows:
var found = false var found = false
for l in leftRows: for l in leftRows:
let merged = mergeRow(l, r, leftAlias, rightAlias) let merged = mergeRow(l, r, leftAlias, rightAlias, leftTable, rightTable)
if plan.joinCond == nil or evalExpr(plan.joinCond, merged, ctx) == "true": if plan.joinCond == nil or evalExpr(plan.joinCond, merged, ctx) == "true":
found = true found = true
break break
@@ -3568,12 +3632,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var minVal = "" var minVal = ""
for row in matchingRows: for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx) let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if minVal == "" or cmpMin(v, minVal): minVal = v if minVal == "" or cmpMin(v, minVal): minVal = v
aggResult = minVal aggResult = minVal
of irMax: of irMax:
var maxVal = "" var maxVal = ""
for row in matchingRows: for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx) let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if maxVal == "" or cmpMax(v, maxVal): maxVal = v if maxVal == "" or cmpMax(v, maxVal): maxVal = v
aggResult = maxVal aggResult = maxVal
else: discard else: discard
@@ -3878,7 +3944,7 @@ proc getSelectColumns(stmt: Node): seq[string] =
elif e.kind == nkIdent: elif e.kind == nkIdent:
result.add(e.identName) result.add(e.identName)
elif e.kind == nkPath and e.pathParts.len > 0: elif e.kind == nkPath and e.pathParts.len > 0:
result.add(e.pathParts[^1]) result.add(e.pathParts.join("."))
elif e.kind == nkFuncCall: elif e.kind == nkFuncCall:
var aliasArgs: seq[string] = @[] var aliasArgs: seq[string] = @[]
for arg in e.funcArgs: for arg in e.funcArgs:
@@ -4740,8 +4806,10 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive: if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
var kvPairs: seq[(string, seq[byte])] var kvPairs: seq[(string, seq[byte])]
for key, version in ctx.pendingTxn.writeSet: for key, version in ctx.pendingTxn.writeSet:
if version.value == @[]: ctx.db.delete(key) if version.value == @[] and version.xmax != TxnId(0):
else: ctx.db.put(key, version.value) ctx.db.delete(key)
else:
ctx.db.put(key, version.value)
kvPairs.add((key, version.value)) kvPairs.add((key, version.value))
discard ctx.txnManager.commit(ctx.pendingTxn) discard ctx.txnManager.commit(ctx.pendingTxn)
ctx.pendingTxn = nil ctx.pendingTxn = nil
+29 -3
View File
@@ -296,6 +296,11 @@ proc parseAddSub(p: var Parser): Node =
proc parseComparison(p: var Parser): Node = proc parseComparison(p: var Parser): Node =
result = p.parseAddSub() result = p.parseAddSub()
# Check for NOT prefix on IN/LIKE/BETWEEN
var negated = false
if p.peek().kind == tkNot:
discard p.advance()
negated = true
# Handle BETWEEN ... AND ... # Handle BETWEEN ... AND ...
if p.peek().kind == tkBetween: if p.peek().kind == tkBetween:
let tok = p.advance() let tok = p.advance()
@@ -303,12 +308,27 @@ proc parseComparison(p: var Parser): Node =
discard p.expect(tkAnd) discard p.expect(tkAnd)
let high = p.parseAddSub() let high = p.parseAddSub()
return Node(kind: nkBetweenExpr, betweenExpr: result, return Node(kind: nkBetweenExpr, betweenExpr: result,
betweenLow: low, betweenHigh: high, line: tok.line, col: tok.col) betweenLow: low, betweenHigh: high,
betweenNegated: negated,
line: tok.line, col: tok.col)
# Handle IN (subquery | list) # Handle IN (subquery | list)
if p.peek().kind == tkIn: if p.peek().kind == tkIn:
let tok = p.advance() let tok = p.advance()
let right = p.parseAddSub() discard p.expect(tkLParen)
let right: Node =
if p.peek().kind == tkSelect:
let sub = p.parseSelect()
discard p.expect(tkRParen)
Node(kind: nkSubquery, subQuery: sub, line: tok.line, col: tok.col)
else:
var elems: seq[Node] = @[]
elems.add(p.parseExpr())
while p.match(tkComma):
elems.add(p.parseExpr())
discard p.expect(tkRParen)
Node(kind: nkArrayLit, arrayElems: elems, line: tok.line, col: tok.col)
return Node(kind: nkInExpr, inLeft: result, inRight: right, return Node(kind: nkInExpr, inLeft: result, inRight: right,
inNegated: negated,
line: tok.line, col: tok.col) line: tok.line, col: tok.col)
# Handle LIKE / ILIKE # Handle LIKE / ILIKE
if p.peek().kind in {tkLike, tkILike}: if p.peek().kind in {tkLike, tkILike}:
@@ -316,7 +336,13 @@ proc parseComparison(p: var Parser): Node =
let tok = p.advance() let tok = p.advance()
let pattern = p.parseAddSub() let pattern = p.parseAddSub()
return Node(kind: nkLikeExpr, likeExpr: result, likePattern: pattern, return Node(kind: nkLikeExpr, likeExpr: result, likePattern: pattern,
likeCaseInsensitive: isILike, line: tok.line, col: tok.col) likeCaseInsensitive: isILike,
likeNegated: negated,
line: tok.line, col: tok.col)
# If we consumed NOT but didn't find IN/LIKE/BETWEEN, put it back is not possible.
# Instead, wrap result in a unary NOT node.
if negated:
result = Node(kind: nkUnaryOp, unOp: ukNot, unOperand: result, line: 0, col: 0)
# Handle IS NULL / IS NOT NULL # Handle IS NULL / IS NOT NULL
if p.peek().kind == tkIs: if p.peek().kind == tkIs:
let tok = p.advance() let tok = p.advance()
+185 -3
View File
@@ -1,8 +1,7 @@
## NimForum Adapter Smoke Test ## NimForum Adapter Smoke Test — comprehensive validation
import std/unittest import std/unittest
import std/osproc import std/osproc
import std/os import std/os
import std/strutils
import std/strtabs import std/strtabs
import std/times import std/times
@@ -24,7 +23,23 @@ suite "NimForum Adapter Smoke Test":
env["BARADB_DATA_DIR"] = dataDir env["BARADB_DATA_DIR"] = dataDir
env["BARADB_LOG_LEVEL"] = "error" env["BARADB_LOG_LEVEL"] = "error"
serverProcess = startProcess("./build/baradadb", env=env, options={poStdErrToStdOut, poDaemon}) serverProcess = startProcess("./build/baradadb", env=env, options={poStdErrToStdOut, poDaemon})
sleep(800) # Wait for server to be ready (with timeout)
var pingDb: DbConn
var connected = false
for i in 0 ..< 50:
sleep(100)
try:
pingDb = open("127.0.0.1:" & $port, "", "", "default")
connected = true
break
except:
discard
if not connected:
serverProcess.terminate()
discard serverProcess.waitForExit()
removeDir(dataDir)
raise newException(IOError, "Server failed to start")
pingDb.close()
teardown: teardown:
if serverProcess != nil: if serverProcess != nil:
@@ -67,3 +82,170 @@ suite "NimForum Adapter Smoke Test":
check row[row.len - 1] == "ten" check row[row.len - 1] == "ten"
db.close() db.close()
test "getRow returns @[] for missing rows":
var db = open("127.0.0.1:" & $port, "", "", "default")
db.exec(sql"CREATE TABLE nf_missing (id INT PRIMARY KEY)")
let row = db.getRow(sql"SELECT * FROM nf_missing WHERE id = 999")
check row.len == 0
db.close()
test "insertID and nextId":
var db = open("127.0.0.1:" & $port, "", "", "default")
db.exec(sql"CREATE TABLE nf_ids (id INT PRIMARY KEY, name STRING)")
let id1 = db.insertID(sql"INSERT INTO nf_ids (id, name) VALUES (5, 'five')")
check id1 == 5
let next = db.nextId("nf_ids")
check next == 6
db.close()
test "Transactions (BEGIN/COMMIT/ROLLBACK)":
var db = open("127.0.0.1:" & $port, "", "", "default")
db.exec(sql"CREATE TABLE nf_txn (id INT PRIMARY KEY)")
db.dbBegin()
db.exec(sql"INSERT INTO nf_txn (id) VALUES (1)")
db.dbCommit()
check db.getValue(sql"SELECT count(*) FROM nf_txn") == "1"
db.dbBegin()
db.exec(sql"INSERT INTO nf_txn (id) VALUES (2)")
db.dbRollback()
check db.getValue(sql"SELECT count(*) FROM nf_txn") == "1"
db.close()
test "fastRows iterator":
var db = open("127.0.0.1:" & $port, "", "", "default")
db.exec(sql"CREATE TABLE nf_iter (id INT PRIMARY KEY)")
db.exec(sql"INSERT INTO nf_iter (id) VALUES (1)")
db.exec(sql"INSERT INTO nf_iter (id) VALUES (2)")
db.exec(sql"INSERT INTO nf_iter (id) VALUES (3)")
var count = 0
for r in db.fastRows(sql"SELECT id FROM nf_iter ORDER BY id"):
count.inc
check count == 3
db.close()
test "execAffectedRows":
var db = open("127.0.0.1:" & $port, "", "", "default")
db.exec(sql"CREATE TABLE nf_aff (id INT PRIMARY KEY, name STRING)")
db.exec(sql"INSERT INTO nf_aff (id, name) VALUES (1, 'a')")
db.exec(sql"INSERT INTO nf_aff (id, name) VALUES (2, 'b')")
let affected = db.execAffectedRows(sql"DELETE FROM nf_aff WHERE id = 1")
check affected == 1
db.close()
test "tryExec success and failure":
var db = open("127.0.0.1:" & $port, "", "", "default")
db.exec(sql"CREATE TABLE nf_try (id INT PRIMARY KEY)")
check db.tryExec(sql"INSERT INTO nf_try (id) VALUES (1)") == true
check db.tryExec(sql"INSERT INTO nf_try (id) VALUES (1)") == false
db.close()
test "NimForum schema creation":
var db = open("127.0.0.1:" & $port, "", "", "default")
# Create nimforum-like schema (adapter preprocesses DATETIME/TIMESTAMP/INET)
db.exec(sql"""
CREATE TABLE thread (
id INT PRIMARY KEY,
name STRING NOT NULL,
views INT NOT NULL,
modified DATETIME NOT NULL DEFAULT DATETIME('now')
)
""")
db.exec(sql"CREATE UNIQUE INDEX ThreadNameIx ON thread (name)")
db.exec(sql"""
CREATE TABLE person (
id INT PRIMARY KEY,
name STRING NOT NULL,
password STRING NOT NULL,
email STRING NOT NULL,
creation DATETIME NOT NULL DEFAULT DATETIME('now'),
salt STRING NOT NULL,
user_status STRING NOT NULL,
lastOnline DATETIME NOT NULL DEFAULT DATETIME('now'),
ban STRING NOT NULL DEFAULT ''
)
""")
db.exec(sql"CREATE UNIQUE INDEX UserNameIx ON person (name)")
db.exec(sql"""
CREATE TABLE post (
id INT PRIMARY KEY,
author INT NOT NULL,
ip STRING NOT NULL,
header STRING NOT NULL,
content STRING NOT NULL,
thread INT NOT NULL,
creation DATETIME NOT NULL DEFAULT DATETIME('now')
)
""")
db.exec(sql"""
CREATE TABLE session (
id INT PRIMARY KEY,
ip STRING NOT NULL,
password STRING NOT NULL,
userid INT NOT NULL,
lastModified DATETIME NOT NULL DEFAULT DATETIME('now')
)
""")
db.exec(sql"""
CREATE TABLE antibot (
id INT PRIMARY KEY,
ip STRING NOT NULL,
answer STRING NOT NULL,
created DATETIME NOT NULL DEFAULT DATETIME('now')
)
""")
db.exec(sql"CREATE INDEX PersonStatusIdx ON person(user_status)")
db.exec(sql"CREATE INDEX PostByAuthorIdx ON post(thread, author)")
# Insert and query forum data
db.exec(sql"INSERT INTO thread (id, name, views) VALUES (1, 'First Thread', 0)")
db.exec(sql"INSERT INTO person (id, name, password, email, salt, user_status, ban) VALUES (1, 'admin', 'pw', 'a@b.c', 'salt', 'active', '')")
db.exec(sql"INSERT INTO post (id, author, ip, header, content, thread) VALUES (1, 1, '127.0.0.1', 'Hello', 'World', 1)")
let threads = db.getAllRows(sql"SELECT * FROM thread")
check threads.len == 1
let posts = db.getAllRows(sql"SELECT * FROM post WHERE thread = 1")
check posts.len == 1
db.close()
test "NimForum-like operations":
var db = open("127.0.0.1:" & $port, "", "", "default")
db.exec(sql"CREATE TABLE nf_ops (id INT PRIMARY KEY, views INT NOT NULL)")
db.exec(sql"INSERT INTO nf_ops (id, views) VALUES (1, 0)")
# Update views (like nimforum does)
let affected = db.execAffectedRows(sql"UPDATE nf_ops SET views = views + 1 WHERE id = 1")
check affected == 1
let views = db.getValue(sql"SELECT views FROM nf_ops WHERE id = 1")
check views == "1"
# Try update with DATETIME('now') (preprocessed by adapter)
db.exec(sql"CREATE TABLE nf_dt (id INT PRIMARY KEY, modified DATETIME)")
db.exec(sql"INSERT INTO nf_dt (id, modified) VALUES (1, DATETIME('now'))")
let dtRow = db.getRow(sql"SELECT modified FROM nf_dt WHERE id = 1")
check dtRow.len == 1
check dtRow[0].len > 0
db.close()
+73 -29
View File
@@ -1,73 +1,104 @@
## Stress Test — parallel workloads against LSM-Tree ## Stress Test — parallel workloads against shared LSM-Tree
## Each worker gets its own LSMTree instance (thread-safe via internal locks). ## Validates correctness under concurrent access.
import std/os import std/os
import std/random import std/random
import std/strutils
import std/times import std/times
import std/monotimes import std/monotimes
import std/locks
import barabadb/storage/lsm import barabadb/storage/lsm
const const
NumWorkers = 10 NumWorkers = 16
OpsPerWorker = 1000 OpsPerWorker = 2000
KeySpace = 500 KeySpace = 1000
type type
WorkerArgs = tuple WorkerArgs = tuple
workerId: int workerId: int
dataDir: string db: ptr LSMTree
errors: ptr int errors: ptr int
lock: ptr Lock
randomize() randomize()
proc runWorker(args: WorkerArgs) {.thread.} = proc runWorker(args: WorkerArgs) {.thread.} =
var db: LSMTree
var localErrors = 0 var localErrors = 0
{.cast(gcsafe).}: var rng = initRand(args.workerId)
db = newLSMTree(args.dataDir)
for i in 0 ..< OpsPerWorker: for i in 0 ..< OpsPerWorker:
let op = rand(0 .. 2) let op = rng.rand(0 .. 3)
let key = "key_" & $args.workerId & "_" & $(rand(0 ..< KeySpace)) let key = "key_" & $(rng.rand(0 ..< KeySpace))
let value = cast[seq[byte]]("val_" & $i & "_" & $args.workerId) let value = cast[seq[byte]]("val_" & $i & "_" & $args.workerId)
case op case op
of 0: of 0:
db.put(key, value) # Put
acquire(args.lock[])
try:
args.db[].put(key, value)
except:
localErrors.inc
finally:
release(args.lock[])
of 1: of 1:
let (found, val) = db.get(key) # Get and verify (if found, value should be from some worker)
acquire(args.lock[])
var found = false
var val: seq[byte]
try:
(found, val) = args.db[].get(key)
except:
localErrors.inc
finally:
release(args.lock[])
if found: if found:
if val != value and val.len > 0: let valStr = cast[string](val)
discard if not valStr.startsWith("val_"):
localErrors.inc
of 2: of 2:
db.delete(key) # Delete
acquire(args.lock[])
try:
args.db[].delete(key)
except:
localErrors.inc
finally:
release(args.lock[])
of 3:
# Overwrite
acquire(args.lock[])
try:
args.db[].put(key, value)
except:
localErrors.inc
finally:
release(args.lock[])
else: else:
discard discard
for k in 0 ..< KeySpace:
let key = "key_" & $args.workerId & "_" & $k
let (found, _) = db.get(key)
discard found
db.close()
args.errors[] = localErrors args.errors[] = localErrors
proc main() = proc main() =
let baseDir = "/tmp/baradb_stress_test" let baseDir = "/tmp/baradb_stress_test_shared"
removeDir(baseDir) removeDir(baseDir)
createDir(baseDir) createDir(baseDir)
let start = getMonoTime() let start = getMonoTime()
var workerDirs: seq[string] = @[] # Single shared database
for i in 0 ..< NumWorkers: var db = newLSMTree(baseDir)
workerDirs.add(baseDir / "worker_" & $i) var dbPtr = addr db
var dbLock: Lock
initLock(dbLock)
var lockPtr = addr dbLock
# Run workers in parallel using std/threads # Run workers in parallel using std/threads
var threadArr: array[NumWorkers, Thread[WorkerArgs]] var threadArr: array[NumWorkers, Thread[WorkerArgs]]
var errorCounts: array[NumWorkers, int] var errorCounts: array[NumWorkers, int]
for i in 0 ..< NumWorkers: for i in 0 ..< NumWorkers:
let args: WorkerArgs = (workerId: i, dataDir: workerDirs[i], errors: addr errorCounts[i]) let args: WorkerArgs = (workerId: i, db: dbPtr, errors: addr errorCounts[i], lock: lockPtr)
createThread(threadArr[i], runWorker, args) createThread(threadArr[i], runWorker, args)
var totalErrors = 0 var totalErrors = 0
@@ -75,15 +106,28 @@ proc main() =
joinThread(threadArr[i]) joinThread(threadArr[i])
totalErrors += errorCounts[i] totalErrors += errorCounts[i]
# Verify: scan all keys and ensure no corruption
var scanErrors = 0
for k in 0 ..< KeySpace:
let key = "key_" & $k
let (found, val) = db.get(key)
if found:
let valStr = cast[string](val)
if not valStr.startsWith("val_"):
scanErrors.inc
db.close()
let elapsed = (getMonoTime() - start).inMilliseconds let elapsed = (getMonoTime() - start).inMilliseconds
let totalOps = NumWorkers * OpsPerWorker let totalOps = NumWorkers * OpsPerWorker
echo "Stress test completed: ", totalOps, " ops across ", NumWorkers, " workers" echo "Stress test completed: ", totalOps, " ops across ", NumWorkers, " workers"
echo "Time: ", elapsed, " ms" echo "Time: ", elapsed, " ms"
echo "Throughput: ", float(totalOps) / (float(elapsed) / 1000.0), " ops/sec" echo "Throughput: ", float(totalOps) / (float(elapsed) / 1000.0), " ops/sec"
echo "Errors: ", totalErrors echo "Runtime errors: ", totalErrors
echo "Scan errors: ", scanErrors
if totalErrors > 0: if totalErrors > 0 or scanErrors > 0:
quit(1) quit(1)
removeDir(baseDir) removeDir(baseDir)