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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user