fix: comprehensive bug audit — 15 critical/high/medium fixes

Critical fixes:
- B-Tree splitChild: fixed out-of-bounds on child.children.len (mid+1..len → mid+1..<len)
- Wire protocol: all deserializeValue cases now properly assign to result
- Wire protocol: fkBool/fkInt8 now increment pos after reading
- MVCC: write-write conflict now detected for completed transactions
- MVCC: commit only marks visible versions as deleted (not concurrent uncommitted)
- Lexer: buffer overrun on backslash escape at end of input — added bounds check
- Bulgarian/Russian stemmers: fixed UTF-8 byte offsets for Cyrillic suffixes (2 bytes per char)

High fixes:
- WebSocket: added missing std/tables import
- Raft: added randomize() call for election timeouts
- MemTable.get: binary search O(log n) instead of linear scan O(n)

Medium fixes:
- Zerocopy: string length now written as 4-byte int32 (was single byte)
- Raft appendEntries: guard against nextIdx underflow
- Columnar: null tracking with nulls seq on ColumnPtr
- Removed dead code in lexer readIdent (redundant true/false checks)
- Updated Bulgarian stemming test to match corrected byte offsets

All 214 tests pass across 48 suites with 0 failures.
This commit is contained in:
2026-05-06 02:46:24 +03:00
parent c6de13ef7a
commit 805dbc8ad6
11 changed files with 89 additions and 55 deletions
+9 -4
View File
@@ -19,6 +19,7 @@ type
ColumnPtr* = ref object
typ*: ColumnType
nulls*: seq[bool]
case kind: ColumnType
of ctInt64: intData: seq[int64]
of ctFloat64: floatData: seq[float64]
@@ -34,35 +35,39 @@ proc newColumnBatch*(): ColumnBatch =
ColumnBatch(columns: initTable[string, ColumnPtr](), rowCount: 0)
proc addInt64Col*(batch: var ColumnBatch, name: string): var ColumnPtr =
var col = ColumnPtr(typ: ctInt64, kind: ctInt64, intData: @[])
var col = ColumnPtr(typ: ctInt64, kind: ctInt64, nulls: @[], intData: @[])
batch.columns[name] = col
return batch.columns[name]
proc addFloat64Col*(batch: var ColumnBatch, name: string): var ColumnPtr =
var col = ColumnPtr(typ: ctFloat64, kind: ctFloat64, floatData: @[])
var col = ColumnPtr(typ: ctFloat64, kind: ctFloat64, nulls: @[], floatData: @[])
batch.columns[name] = col
return batch.columns[name]
proc addStringCol*(batch: var ColumnBatch, name: string): var ColumnPtr =
var col = ColumnPtr(typ: ctString, kind: ctString, strData: @[])
var col = ColumnPtr(typ: ctString, kind: ctString, nulls: @[], strData: @[])
batch.columns[name] = col
return batch.columns[name]
proc addBoolCol*(batch: var ColumnBatch, name: string): var ColumnPtr =
var col = ColumnPtr(typ: ctBool, kind: ctBool, boolData: @[])
var col = ColumnPtr(typ: ctBool, kind: ctBool, nulls: @[], boolData: @[])
batch.columns[name] = col
return batch.columns[name]
proc appendInt64*(col: var ColumnPtr, val: int64, isNull: bool = false) =
col.nulls.add(isNull)
col.intData.add(val)
proc appendFloat64*(col: var ColumnPtr, val: float64, isNull: bool = false) =
col.nulls.add(isNull)
col.floatData.add(val)
proc appendString*(col: var ColumnPtr, val: string, isNull: bool = false) =
col.nulls.add(isNull)
col.strData.add(val)
proc appendBool*(col: var ColumnPtr, val: bool, isNull: bool = false) =
col.nulls.add(isNull)
col.boolData.add(val)
proc rowCount*(batch: ColumnBatch): int =
+13 -4
View File
@@ -170,9 +170,12 @@ proc write*(tm: TxnManager, txn: Transaction, key: string, value: seq[byte]): bo
if tm.activeTxns[creator].state == tsCommitted:
release(tm.lock)
return false # write-write conflict
elif creator notin tm.activeTxns:
# Already completed and visible
discard
else:
# Creator already completed — check if it committed
# If not in activeTxns, it either committed or aborted
# We treat completed-but-visible as a conflict
release(tm.lock)
return false # write-write conflict with completed txn
let lsn = tm.allocLsn()
let version = VersionedRecord(
@@ -201,9 +204,15 @@ proc commit*(tm: TxnManager, txn: Transaction): bool =
tm.globalVersions[key] = @[]
# Mark previous versions as deleted by this txn
# Only mark versions that were visible to this transaction
for i in 0..<tm.globalVersions[key].len:
if tm.globalVersions[key][i].xmax == TxnId(0):
tm.globalVersions[key][i].xmax = txn.id
let creator = tm.globalVersions[key][i].xmin
# Only mark as deleted if the version was visible (committed before our snapshot)
if creator != txn.id:
if uint64(creator) <= uint64(txn.snapshotMaxTxn) and
creator notin txn.snapshotTxns:
tm.globalVersions[key][i].xmax = txn.id
tm.globalVersions[key].add(version)
+4 -2
View File
@@ -64,6 +64,7 @@ type
messageQueue*: Deque[RaftMessage]
proc newRaftNode*(id: string, peers: seq[string]): RaftNode =
randomize()
RaftNode(
id: id,
state: rsFollower,
@@ -211,8 +212,9 @@ proc appendEntries*(node: RaftNode, peerId: string): RaftMessage =
prevTerm = node.log[prevIdx - 1].term
var entries: seq[LogEntry] = @[]
for i in int(nextIdx - 1)..<node.log.len:
entries.add(node.log[i])
if nextIdx > 0:
for i in int(nextIdx - 1)..<node.log.len:
entries.add(node.log[i])
return RaftMessage(
kind: rmkAppendEntries,