fix: deadlock detector, graph/query/lexer бъгове

Поправени проблеми:
- Deadlock detector: cycle reconstruction вече проверява пълен цикъл
- Graph cypher: executeCypher acquire-ва lock при четене
- Graph engine: loadFromFile acquire-ва lock при зареждане
- Graph engine: lock поле е exported за външен достъп
- Query: parseRowData поддържа escape-ване на , и = в стойности
- Query: execUpdateRow escape-ва стойности при сериализация
- Lexer: readNumber спира при втора точка (1.2.3 вече не е валиден float)
- Lexer: skipBlockComment хвърля ValueError при незатворен коментар

292 теста, 0 failure-а.
This commit is contained in:
2026-05-12 23:21:29 +03:00
parent 80d57a36bd
commit 5e585dd347
5 changed files with 55 additions and 4 deletions
+7
View File
@@ -73,7 +73,14 @@ proc detectCycle*(dd: DeadlockDetector): seq[uint64] =
while parent.getOrDefault(current, 0'u64) != neighbor and while parent.getOrDefault(current, 0'u64) != neighbor and
parent.getOrDefault(current, 0'u64) != 0: parent.getOrDefault(current, 0'u64) != 0:
current = parent[current] current = parent[current]
if current == 0: break
cycle.add(current) cycle.add(current)
# Verify we actually closed the cycle back to neighbor
if cycle[^1] != neighbor and parent.getOrDefault(cycle[^1], 0'u64) == neighbor:
cycle.add(neighbor)
elif cycle[^1] != neighbor:
# Incomplete cycle — should not happen with valid parent chain
return @[]
cycle.reverse() cycle.reverse()
return cycle return cycle
if neighbor notin visited: if neighbor notin visited:
+4
View File
@@ -1,6 +1,7 @@
## Cypher-like Graph Query Extension for BaraQL ## Cypher-like Graph Query Extension for BaraQL
import std/tables import std/tables
import std/strutils import std/strutils
import std/locks
import engine import engine
type type
@@ -179,6 +180,9 @@ proc executeCypher*(g: Graph, query: CypherQuery): CypherResult =
if query.pattern.nodes.len == 0: if query.pattern.nodes.len == 0:
return return
acquire(g.lock)
defer: release(g.lock)
# Basic MATCH execution # Basic MATCH execution
if query.kind == "MATCH": if query.kind == "MATCH":
# For each node matching the pattern, collect results # For each node matching the pattern, collect results
+3 -1
View File
@@ -38,7 +38,7 @@ type
reverseAdj*: Table[NodeId, seq[AdjacencyEntry]] # incoming reverseAdj*: Table[NodeId, seq[AdjacencyEntry]] # incoming
nextNodeId: uint64 nextNodeId: uint64
nextEdgeId: uint64 nextEdgeId: uint64
lock: Lock lock*: Lock
proc `==`*(a, b: EdgeId): bool = uint64(a) == uint64(b) proc `==`*(a, b: EdgeId): bool = uint64(a) == uint64(b)
proc `==`*(a, b: NodeId): bool = uint64(a) == uint64(b) proc `==`*(a, b: NodeId): bool = uint64(a) == uint64(b)
@@ -352,6 +352,7 @@ proc loadFromFile*(path: string): Graph =
lock: Lock(), lock: Lock(),
) )
initLock(result.lock) initLock(result.lock)
acquire(result.lock)
for i in 0 ..< nodeCount: for i in 0 ..< nodeCount:
let id = NodeId(s.readUint64()) let id = NodeId(s.readUint64())
@@ -385,4 +386,5 @@ proc loadFromFile*(path: string): Graph =
result.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src, result.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src,
weight: weight, label: label)) weight: weight, label: label))
release(result.lock)
s.close() s.close()
+38 -3
View File
@@ -406,14 +406,49 @@ proc getValue(values: seq[string], fields: seq[string], colName: string): string
proc isNull*(value: string): bool = proc isNull*(value: string): bool =
value.len == 0 or value.toLower() == "null" value.len == 0 or value.toLower() == "null"
proc escapeRowVal(v: string): string =
v.replace("\\", "\\\\").replace(",", "\\,").replace("=", "\\=")
proc unescapeRowVal(v: string): string =
result = ""
var i = 0
while i < v.len:
if v[i] == '\\' and i + 1 < v.len:
case v[i+1]
of '\\', ',', '=':
result &= v[i+1]
i += 2
continue
else: discard
result &= v[i]
inc i
proc parseRowData(valStr: string): Table[string, string] = proc parseRowData(valStr: string): Table[string, string] =
## Parse "col1=val1,col2=val2" into a table ## Parse "col1=val1,col2=val2" into a table
result = initTable[string, string]() result = initTable[string, string]()
for part in valStr.split(","): var i = 0
var part = ""
while i < valStr.len:
if valStr[i] == '\\' and i + 1 < valStr.len:
part &= valStr[i]
part &= valStr[i+1]
i += 2
continue
if valStr[i] == ',':
let eqPos = part.find('=') let eqPos = part.find('=')
if eqPos >= 0: if eqPos >= 0:
let k = part[0..<eqPos].strip() let k = part[0..<eqPos].strip()
let v = part[eqPos+1..^1].strip() let v = unescapeRowVal(part[eqPos+1..^1].strip())
result[k] = v
part = ""
else:
part &= valStr[i]
inc i
if part.len > 0:
let eqPos = part.find('=')
if eqPos >= 0:
let k = part[0..<eqPos].strip()
let v = unescapeRowVal(part[eqPos+1..^1].strip())
result[k] = v result[k] = v
proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row]
@@ -844,7 +879,7 @@ proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Tab
return 0 return 0
var parts: seq[string] = @[] var parts: seq[string] = @[]
for col, val in parsed: for col, val in parsed:
parts.add(col & "=" & val) parts.add(col & "=" & escapeRowVal(val))
let newVal = parts.join(",") let newVal = parts.join(",")
# Update indexes: remove old, insert new # Update indexes: remove old, insert new
for colName in ctx.btrees.keys.toSeq(): for colName in ctx.btrees.keys.toSeq():
+3
View File
@@ -363,6 +363,7 @@ proc skipBlockComment(l: var Lexer) =
discard l.advance() discard l.advance()
return return
discard l.advance() discard l.advance()
raise newException(ValueError, "Unclosed block comment at line " & $l.line & ", col " & $l.col)
proc readString(l: var Lexer, quote: char): string = proc readString(l: var Lexer, quote: char): string =
result = "" result = ""
@@ -389,6 +390,8 @@ proc readNumber(l: var Lexer, startLine, startCol: int): Token =
var isFloat = false var isFloat = false
while l.pos < l.input.len and (l.input[l.pos] in Digits or l.input[l.pos] == '.'): while l.pos < l.input.len and (l.input[l.pos] in Digits or l.input[l.pos] == '.'):
if l.input[l.pos] == '.': if l.input[l.pos] == '.':
if isFloat:
break # second dot ends the number
isFloat = true isFloat = true
numStr.add(l.input[l.pos]) numStr.add(l.input[l.pos])
discard l.advance() discard l.advance()