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
+123 -55
View File
@@ -252,6 +252,8 @@ proc exprToSql(node: Node): string =
return node.funcName & "()"
of nkUnaryOp:
return $node.unOp & " " & exprToSql(node.unOperand)
of nkPath:
return node.pathParts.join(".")
else:
return $node.kind
@@ -956,7 +958,12 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
if parseFloat(left) == parseFloat(right): return "true"
except: discard
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:
try:
return if parseFloat(left) < parseFloat(right): "true" else: "false"
@@ -2374,41 +2381,73 @@ proc lowerExpr*(node: Node): IRExpr =
result.binOp = if node.likeCaseInsensitive: irILike else: irLike
result.binLeft = lowerExpr(node.likeExpr)
result.binRight = lowerExpr(node.likePattern)
if node.likeNegated:
let wrapped = result
result = IRExpr(kind: irekUnary, valueKind: vkBool, unOp: irNot, unExpr: wrapped)
of nkBetweenExpr:
result = IRExpr(kind: irekBinary, valueKind: vkBool)
result.binOp = irAnd
let leftCmp = IRExpr(kind: irekBinary, valueKind: vkBool)
leftCmp.binOp = irGte
leftCmp.binLeft = lowerExpr(node.betweenExpr)
leftCmp.binRight = lowerExpr(node.betweenLow)
let rightCmp = IRExpr(kind: irekBinary, valueKind: vkBool)
rightCmp.binOp = irLte
rightCmp.binLeft = lowerExpr(node.betweenExpr)
rightCmp.binRight = lowerExpr(node.betweenHigh)
result.binLeft = leftCmp
result.binRight = rightCmp
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.binOp = irAnd
let leftCmp = IRExpr(kind: irekBinary, valueKind: vkBool)
leftCmp.binOp = irGte
leftCmp.binLeft = lowerExpr(node.betweenExpr)
leftCmp.binRight = lowerExpr(node.betweenLow)
let rightCmp = IRExpr(kind: irekBinary, valueKind: vkBool)
rightCmp.binOp = irLte
rightCmp.binLeft = lowerExpr(node.betweenExpr)
rightCmp.binRight = lowerExpr(node.betweenHigh)
result.binLeft = leftCmp
result.binRight = rightCmp
of nkInExpr:
if node.inRight.kind == nkArrayLit:
result = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkBool, boolVal: false))
for elem in node.inRight.arrayElems:
let eqCmp = IRExpr(kind: irekBinary)
eqCmp.binOp = irEq
eqCmp.binLeft = lowerExpr(node.inLeft)
eqCmp.binRight = lowerExpr(elem)
let orNode = IRExpr(kind: irekBinary)
orNode.binOp = irOr
orNode.binLeft = result
orNode.binRight = eqCmp
result = orNode
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))
for elem in node.inRight.arrayElems:
let eqCmp = IRExpr(kind: irekBinary)
eqCmp.binOp = irEq
eqCmp.binLeft = lowerExpr(node.inLeft)
eqCmp.binRight = lowerExpr(elem)
let orNode = IRExpr(kind: irekBinary)
orNode.binOp = irOr
orNode.binLeft = result
orNode.binRight = eqCmp
result = orNode
elif node.inRight.kind == nkSubquery:
result = IRExpr(kind: irekBinary)
result.binOp = irIn
result.binOp = if node.inNegated: irNotIn else: irIn
result.binLeft = lowerExpr(node.inLeft)
result.binRight = IRExpr(kind: irekSubquery)
result.binRight.subqueryPlan = lowerSelect(node.inRight.subQuery)
else:
result = IRExpr(kind: irekBinary)
result.binOp = irEq
result.binOp = if node.inNegated: irNeq else: irEq
result.binLeft = lowerExpr(node.inLeft)
result.binRight = lowerExpr(node.inRight)
of nkExists:
@@ -2900,12 +2939,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var minVal = ""
for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if minVal == "" or cmpMin(v, minVal): minVal = v
newRow[alias] = minVal
of irMax:
var maxVal = ""
for row in filteredRows:
let v = evalExpr(expr.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if maxVal == "" or cmpMax(v, maxVal): maxVal = v
newRow[alias] = maxVal
of irArrayAgg:
@@ -2964,20 +3005,23 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
of irpkSort:
var sourceRows = executePlan(ctx, plan.sortSource)
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 =
let va = evalExpr(sortExpr, a, ctx)
let vb = evalExpr(sortExpr, b, ctx)
try:
let fa = parseFloat(va)
let fb = parseFloat(vb)
if fa < fb: return -1
if fa > fb: return 1
return 0
except:
return cmp(va, vb)
sourceRows.sort(sortCmp, if ascending: Ascending else: Descending)
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 vb = evalExpr(sortExpr, b, ctx)
var cmpRes = 0
try:
let fa = parseFloat(va)
let fb = parseFloat(vb)
if fa < fb: cmpRes = -1
elif fa > fb: cmpRes = 1
except:
cmpRes = cmp(va, vb)
if not ascending: cmpRes = -cmpRes
if cmpRes != 0: return cmpRes
return 0
sourceRows.sort(sortCmp, Ascending)
return sourceRows
of irpkLimit:
@@ -3075,12 +3119,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var minVal = ""
for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if minVal == "" or cmpMin(v, minVal): minVal = v
aggRow[aggKey] = minVal
of irMax:
var maxVal = ""
for row in filteredRows:
let v = evalExpr(aggExpr.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if maxVal == "" or cmpMax(v, maxVal): maxVal = v
aggRow[aggKey] = maxVal
of irArrayAgg:
@@ -3107,7 +3153,21 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
let leftRows = executePlan(ctx, plan.joinLeft)
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]()
for k, v in left:
if not k.startsWith("$"):
@@ -3115,17 +3175,20 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
for k, v in right:
if not k.startsWith("$") and k notin result:
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:
if not k.startsWith("$"):
result[leftAlias & "." & k] = v
if rightAlias.len > 0:
result[lq & "." & k] = v
let rq = if rightAlias.len > 0: rightAlias elif rightTable.len > 0: rightTable else: ""
if rq.len > 0:
for k, v in right:
if not k.startsWith("$"):
result[rightAlias & "." & k] = v
result[rq & "." & k] = v
let leftAlias = if plan.joinLeft != nil and plan.joinLeft.kind == irpkScan:
plan.joinLeft.scanAlias else: ""
let leftTable = getLeftmostScanTable(plan.joinLeft)
# LATERAL JOIN: for each left row, scan right, merge, then filter/sort/limit
if plan.joinLateral:
@@ -3169,7 +3232,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
# Merge, filter
var mergedRows: seq[Row]
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":
continue
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:
plan.joinRight.scanAlias else: ""
let rightTable = getScanTable(plan.joinRight)
if plan.joinKind == irjkCross:
for l in leftRows:
for r in rightRows:
result.add(mergeRow(l, r, leftAlias, rightAlias))
result.add(mergeRow(l, r, leftAlias, rightAlias, leftTable, rightTable))
return result
case plan.joinStrategy
@@ -3296,9 +3360,9 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
matchedProbe[i] = true
for brow in hashTable[key]:
if buildIsLeft:
result.add(mergeRow(brow, prow, leftAlias, rightAlias))
result.add(mergeRow(brow, prow, leftAlias, rightAlias, leftTable, rightTable))
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
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:
let parsed = parseRowData(entry.rowValue)
if outerIsLeft:
result.add(mergeRow(orow, parsed, leftAlias, rightAlias))
result.add(mergeRow(orow, parsed, leftAlias, rightAlias, leftTable, rightTable))
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:
matchedOuter[i] = false
@@ -3455,7 +3519,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
for l in leftRows:
var matched = false
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":
result.add(merged)
matched = true
@@ -3479,7 +3543,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
for r in rightRows:
var found = false
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":
found = true
break
@@ -3568,12 +3632,14 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] =
var minVal = ""
for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if minVal == "" or cmpMin(v, minVal): minVal = v
aggResult = minVal
of irMax:
var maxVal = ""
for row in matchingRows:
let v = evalExpr(plan.pivotAgg.aggArgs[0], row, ctx)
if v == "\\\\N": continue
if maxVal == "" or cmpMax(v, maxVal): maxVal = v
aggResult = maxVal
else: discard
@@ -3878,7 +3944,7 @@ proc getSelectColumns(stmt: Node): seq[string] =
elif e.kind == nkIdent:
result.add(e.identName)
elif e.kind == nkPath and e.pathParts.len > 0:
result.add(e.pathParts[^1])
result.add(e.pathParts.join("."))
elif e.kind == nkFuncCall:
var aliasArgs: seq[string] = @[]
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:
var kvPairs: seq[(string, seq[byte])]
for key, version in ctx.pendingTxn.writeSet:
if version.value == @[]: ctx.db.delete(key)
else: ctx.db.put(key, version.value)
if version.value == @[] and version.xmax != TxnId(0):
ctx.db.delete(key)
else:
ctx.db.put(key, version.value)
kvPairs.add((key, version.value))
discard ctx.txnManager.commit(ctx.pendingTxn)
ctx.pendingTxn = nil