Bug fixes: composite PK, nl_to_sql sandbox, FK check, SQL injection, storage correctness

Critical fixes:
- Composite PK: execInsert + validateConstraints use all PK columns
- nl_to_sql: non-SELECT SQL no longer executed directly during validation
- FK check: removed O(N) scanMemTable fallback, uses db.get() with SSTables
- exprToSql: nkIdent wrapped in quotes to prevent SQL injection
- restoreSchema: try/except around tokenize/parse for crash resilience
- recovery.nim: lastTxnId tracking + putUnsafe/deleteUnsafe without WAL
- SCRAM: verifyClientProof length check + DefaultIterationCount restored

Storage fixes:
- lsm.nim: SSTable sort order fixed (ascending), close() flushes all memtables
- compaction.nim: tombstones preserved during compaction
- wal.nim: header written for empty existing files, readEntries checks magic
- btree.nim: B+ tree leaf split keeps boundary key
- bloom.nim: deserialize raises on short data
- mmap.nim: bounds checks for adviseWillNeed/DontNeed + posix.close()

Protocol fixes:
- zerocopy.nim: readString bounds check
- wire.nim: deserializeValue 32-bit underflow check
- auth.nim: JWT JSON escaping for claims
- server.nim: readUint32BE bounds check + specific exception handling
- raft.nim: readData checks + specific exception handling

Tests:
- Added Composite Primary Key test suite (4 tests)

Build: 0 warnings, 0 errors
This commit is contained in:
2026-05-18 11:33:11 +03:00
parent a28c845476
commit 967c0855a5
25 changed files with 376 additions and 171 deletions
+6 -7
View File
@@ -32,6 +32,7 @@ type
dataDir*: string
entries*: seq[RecoveredEntry]
result*: RecoveryResult
lastTxnId*: uint64 # tracks commits seen in WAL
proc newCrashRecovery*(walDir: string, dataDir: string): CrashRecovery =
CrashRecovery(
@@ -101,6 +102,7 @@ proc scanWAL*(rec: CrashRecovery): seq[RecoveredEntry] =
discard
stream.close()
rec.lastTxnId = txnId
proc analyze*(rec: CrashRecovery): RecoveryResult =
rec.entries = rec.scanWAL()
@@ -109,10 +111,7 @@ proc analyze*(rec: CrashRecovery): RecoveryResult =
rec.result = RecoveryResult(state: recDone, applied: false)
return rec.result
var lastCommitted: uint64 = 0
for entry in rec.entries:
if entry.txnId > lastCommitted:
lastCommitted = entry.txnId
var lastCommitted = rec.lastTxnId
var redoCount = 0
var undoCount = 0
@@ -149,11 +148,11 @@ proc recover*(rec: CrashRecovery, db: LSMTree = nil): RecoveryResult =
var undoCount = 0
for entry in rec.entries:
if entry.txnId < analysis.lastTxn:
# Committed — redo
# Committed — redo (bypass WAL to avoid duplicate entries)
if entry.isDelete:
db.delete(entry.key)
db.deleteUnsafe(entry.key)
else:
db.put(entry.key, entry.value)
db.putUnsafe(entry.key, entry.value)
inc redoCount
else:
# Uncommitted — skip (undo)