fix: resolve all 55 identified bugs across the codebase

Comprehensive bug fix pass across all subsystems:
- Critical: lexer infinite loop, WAL lock discipline, checkpoint safety,
  compaction verification, HMAC key truncation, healthCheck leak
- High: MVCC lock safety, B-Tree delete rebalancing, Raft stale term
  handling, replication socket leaks and ack cleanup, sharding migration
  with old assignments, 2PC recovery, JWT/SCRAM auth fixes, wire protocol
  bounds checks
- Medium: config error handling, MERGE parser completeness, IR/codegen
  correctness, UDF bounds checks, LIMIT cost accuracy, mmap safety
- Low: timeout handling, float parsing edge cases, uint32 truncation,
  cache entry cleanup

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 10:59:51 +03:00
parent 7fc6adbe47
commit 2d310a33a1
23 changed files with 765 additions and 207 deletions
+4
View File
@@ -229,8 +229,11 @@ type
mergeSourceAlias*: string
mergeOn*: Node
mergeMatchedUpdate*: seq[Node] # SET assignments, empty if no UPDATE
mergeMatchedDelete*: bool # WHEN MATCHED THEN DELETE
mergeMatchedCondition*: Node # optional AND <condition> after MATCHED
mergeNotMatchedInsert*: seq[Node] # column list for INSERT
mergeNotMatchedValues*: seq[Node] # value expressions for INSERT
mergeNotMatchedNothing*: bool # WHEN NOT MATCHED THEN DO NOTHING
of nkCreateType:
ctName*: string
ctBases*: seq[string]
@@ -404,6 +407,7 @@ type
funcName*: string
funcArgs*: seq[Node]
funcFilter*: Node
funcDistinct*: bool
of nkTypeCast:
castType*: string
castExpr*: Node
+24 -4
View File
@@ -52,17 +52,33 @@ proc codegenExpr*(expr: IRExpr): StorageOp =
return nil
case expr.kind
of irekLiteral:
return nil
let op = newStorageOp(sokProject)
if expr.literal.kind == vkString:
op.columns = @[expr.literal.strVal]
return op
of irekField:
return nil
let op = newStorageOp(sokProject)
op.columns = @[]
for p in expr.fieldPath:
op.columns.add(p)
return op
of irekUnary:
return codegenExpr(expr.unExpr)
let child = codegenExpr(expr.unExpr)
if child != nil:
return child
return nil
of irekBinary:
let left = codegenExpr(expr.binLeft)
let right = codegenExpr(expr.binRight)
if left != nil:
return left
if right != nil:
return right
return nil
of irekAggregate:
return nil
let op = newStorageOp(sokAggregate)
op.aggFuncs = @[("agg", expr.aggOp)]
return op
else:
return nil
@@ -243,6 +259,10 @@ proc estimateCost*(op: StorageOp): float64 =
var cost = 0.0
for child in op.children:
cost += estimateCost(child)
# High offset means more rows must be scanned before limiting
# LIMIT 10 OFFSET 0 -> cheap; LIMIT 10 OFFSET 9990 -> expensive (scan ~10000)
if op.offset > 0:
return cost * (1.0 - 0.5 * float64(op.limit) / float64(op.limit + op.offset))
return cost * 0.5
of sokHashJoin:
var cost = 0.0
+3 -3
View File
@@ -293,10 +293,10 @@ proc inferExpr*(tc: TypeChecker, expr: IRExpr, context: Table[string, IRType]):
if operandType == nil:
return nil
case expr.unOp
of irEq, irNeq, irLt, irLte, irGt, irGte, irAnd, irOr, irNot,
irIsNull, irIsNotNull, irIn, irNotIn, irLike, irILike, irBetween,
irFtsMatch:
of irNot, irIsNull, irIsNotNull:
return IRType(name: "bool", kind: itkScalar)
of irNeg:
return operandType
else:
return nil
of irekBinary:
+7
View File
@@ -164,6 +164,8 @@ type
tkDatabases
tkUse
tkShow
tkDo
tkNothing
tkAutoIncrement
tkSequence
@@ -377,6 +379,8 @@ const keywords*: Table[string, TokenKind] = {
"dst": tkDst,
"merge": tkMerge,
"matched": tkMatched,
"do": tkDo,
"nothing": tkNothing,
"array": tkArray,
"vector": tkVector,
"document": tkDocument,
@@ -516,6 +520,8 @@ proc readNumber(l: var Lexer, startLine, startCol: int): Token =
numStr.add(l.input[l.pos])
discard l.advance()
if isFloat:
if numStr.endsWith("."):
numStr.add("0")
Token(kind: tkFloatLit, value: numStr, line: startLine, col: startCol)
else:
Token(kind: tkIntLit, value: numStr, line: startLine, col: startCol)
@@ -529,6 +535,7 @@ proc readIdent(l: var Lexer, startLine, startCol: int): Token =
fastRuneAt(l.input, l.pos, run, true)
ident.add($run)
inc l.col
discard l.advanceRune()
else:
break
let lowerIdent = ident.toLower()
+38 -23
View File
@@ -15,7 +15,7 @@ proc newParser*(tokens: seq[Token]): Parser =
proc peek(p: Parser): Token =
if p.pos < p.tokens.len:
return p.tokens[p.pos]
Token(kind: tkEof)
return Token(kind: tkEof)
proc advance(p: var Parser): Token =
result = p.tokens[p.pos]
@@ -154,6 +154,7 @@ proc parsePrimary(p: var Parser): Node =
args.add(p.parseExpr())
discard p.expect(tkRParen)
var node = Node(kind: nkFuncCall, funcName: funcName.toLower(), funcArgs: args,
funcDistinct: hasDistinct,
line: tok.line, col: tok.col)
# Handle FILTER (WHERE ...)
if p.match(tkFilter):
@@ -267,8 +268,16 @@ proc parsePostfix(p: var Parser): Node =
result = Node(kind: nkJsonPath, jpLeft: result, jpKey: key, jpAsText: isText,
line: p.peek().line, col: p.peek().col)
proc parseMulDiv(p: var Parser): Node =
proc parsePower(p: var Parser): Node =
result = p.parsePostfix()
if p.peek().kind == tkPower:
let tok = p.advance()
let right = p.parsePower() # right-associative
result = Node(kind: nkBinOp, binOp: bkPow, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseMulDiv(p: var Parser): Node =
result = p.parsePower()
while p.peek().kind in {tkStar, tkSlash, tkPercent, tkFloorDiv}:
let op = case p.peek().kind
of tkStar: bkMul
@@ -339,10 +348,6 @@ proc parseComparison(p: var Parser): Node =
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
if p.peek().kind == tkIs:
let tok = p.advance()
@@ -375,6 +380,9 @@ proc parseComparison(p: var Parser): Node =
let right = p.parseAddSub()
result = Node(kind: nkBinOp, binOp: op, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
# If we consumed NOT but didn't find IN/LIKE/BETWEEN, wrap the full comparison in NOT
if negated:
result = Node(kind: nkUnaryOp, unOp: ukNot, unOperand: result, line: 0, col: 0)
proc parseNot(p: var Parser): Node =
if p.peek().kind == tkNot:
@@ -923,12 +931,28 @@ proc parseMerge(p: var Parser): Node =
result.mergeSourceAlias = p.advance().value
discard p.expect(tkOn)
result.mergeOn = p.parseExpr()
# WHEN MATCHED THEN UPDATE SET ...
if p.match(tkWhen):
if p.match(tkNot):
discard p.expect(tkMatched)
discard p.expect(tkThen)
discard p.expect(tkInsert)
# Parse MERGE branches: WHEN MATCHED [AND cond] THEN ... / WHEN NOT MATCHED [AND cond] THEN ...
while p.match(tkWhen):
var isNotMatched = p.match(tkNot)
discard p.expect(tkMatched)
# Optional AND <condition>
if p.match(tkAnd):
result.mergeMatchedCondition = p.parseExpr()
discard p.expect(tkThen)
if p.peek().kind == tkDelete:
discard p.advance()
if not isNotMatched:
result.mergeMatchedDelete = true
else:
# WHEN NOT MATCHED THEN DELETE is non-standard but treat as no-op
discard
elif p.peek().kind == tkDo:
discard p.advance() # consume DO
discard p.expect(tkNothing)
if isNotMatched:
result.mergeNotMatchedNothing = true
elif p.peek().kind == tkInsert:
discard p.advance()
discard p.expect(tkLParen)
result.mergeNotMatchedInsert.add(Node(kind: nkIdent, identName: p.expect(tkIdent).value))
while p.match(tkComma):
@@ -940,10 +964,8 @@ proc parseMerge(p: var Parser): Node =
while p.match(tkComma):
result.mergeNotMatchedValues.add(p.parseExpr())
discard p.expect(tkRParen)
else:
discard p.expect(tkMatched)
discard p.expect(tkThen)
discard p.expect(tkUpdate)
elif p.peek().kind == tkUpdate:
discard p.advance()
discard p.expect(tkSet)
let col = p.expect(tkIdent).value
discard p.expect(tkEq)
@@ -1078,13 +1100,6 @@ proc parseCreateTable(p: var Parser): Node =
elif p.peek().kind == tkRestrict:
discard p.advance()
cst.cstOnUpdate = "RESTRICT"
elif p.peek().kind == tkSet:
discard p.advance()
discard p.match(tkNull)
cst.cstOnDelete = "SET NULL"
elif p.peek().kind == tkRestrict:
discard p.advance()
cst.cstOnDelete = "RESTRICT"
elif p.peek().kind == tkUpdate:
discard p.advance()
if p.peek().kind == tkCascade:
+25 -7
View File
@@ -77,6 +77,10 @@ proc call*(reg: UDFRegistry, name: string, args: seq[Value]): Value =
inc udf.callCount
if udf.body != nil:
return udf.body(args)
if udf.language == udlExpr:
# Expression-based UDFs are evaluated by the query executor, not here
raise newException(ValueError,
"Expression UDF '" & name & "' must be evaluated via query executor, not direct call")
return Value(kind: vkNull)
proc hasFunction*(reg: UDFRegistry, name: string): bool =
@@ -181,10 +185,13 @@ proc registerStdlib*(reg: UDFRegistry) =
if args.len >= 2 and args[0].kind == vkString and args[1].kind == vkInt64:
let s = args[0].strVal
let start = int(args[1].int64Val)
if start < 0 or start >= s.len:
return Value(kind: vkString, strVal: "")
if args.len >= 3 and args[2].kind == vkInt64:
let length = int(args[2].int64Val)
return Value(kind: vkString, strVal: s[start ..< min(start + length, s.len)])
return Value(kind: vkString, strVal: s[start .. ^1])
let endIdx = min(start + length, s.len)
return Value(kind: vkString, strVal: s[start ..< endIdx])
return Value(kind: vkString, strVal: s[start])
return Value(kind: vkNull))
# Type conversion
@@ -214,21 +221,32 @@ proc registerStdlib*(reg: UDFRegistry) =
UDFParam(name: "value", typeName: "any", required: true)],
"bool", proc(args: seq[Value]): Value =
if args.len >= 2 and args[0].kind == vkArray:
let target = args[1]
for item in args[0].arrayVal:
if item.kind == args[1].kind:
if item.kind == target.kind:
case item.kind
of vkString:
if item.strVal == args[1].strVal:
if item.strVal == target.strVal:
return Value(kind: vkBool, boolVal: true)
of vkInt64:
if item.int64Val == args[1].int64Val:
if item.int64Val == target.int64Val:
return Value(kind: vkBool, boolVal: true)
of vkFloat64:
if item.float64Val == args[1].float64Val:
if item.float64Val == target.float64Val:
return Value(kind: vkBool, boolVal: true)
of vkBool:
if item.boolVal == args[1].boolVal:
if item.boolVal == target.boolVal:
return Value(kind: vkBool, boolVal: true)
else: discard
elif (item.kind in {vkInt64, vkInt32, vkFloat64}) and
(target.kind in {vkInt64, vkInt32, vkFloat64}):
let a = case item.kind of vkInt64: float64(item.int64Val)
of vkInt32: float64(item.int32Val)
else: item.float64Val
let b = case target.kind of vkInt64: float64(target.int64Val)
of vkInt32: float64(target.int32Val)
else: target.float64Val
if a == b:
return Value(kind: vkBool, boolVal: true)
return Value(kind: vkBool, boolVal: false)
return Value(kind: vkNull))