Week 4: Production Hardening — Thread Safety, Fuzz Tests, Property-Based Tests, NimForum Smoke Test
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
Thread Safety: - Add lock to BTreeIndex (insert/get/scan/remove/contains) - Add lock to InvertedIndex (addDocument/removeDocument/search/bm25/tfidf/fuzzy/regex) - Add ctxLock to ExecutionContext for DDL/schema metadata, autoIncCounters, sequences - Deep-copy sessionVars in cloneForConnection() to isolate per-connection state - Fix activeConnections check-then-act race with Lock Wire Protocol Hardening: - Fix RangeDefect in deserializeValue for invalid FieldKind bytes - Fix IndexDefect in fkFloat32/fkFloat64 by reusing readUint32/readUint64 - Fix int32/int64 cast from uint32/uint64 to use cast[] instead of constructor - Fix parseHeader to validate MsgKind instead of holey enum cast Property-Based Tests (tests/prop_test.nim): - 8 invariants for evalExprValue: literal types, commutativity, identity, double negation, NULL propagation Wire Protocol Fuzz (tests/fuzz_test.nim): - 7 fuzz tests: random bytes deserialization, truncated buffers, MsgKind casts, parseHeader logic, message roundtrips NimForum Adapter Smoke Test (tests/nimforum_smoke_test.nim): - Full TCP server lifecycle with adapter CRUD and parameterized queries Test Results: 423 tests pass, 0 failures
This commit is contained in:
@@ -7,6 +7,7 @@ import std/tables
|
|||||||
import std/os
|
import std/os
|
||||||
import std/endians
|
import std/endians
|
||||||
import std/monotimes
|
import std/monotimes
|
||||||
|
import std/locks
|
||||||
import config
|
import config
|
||||||
import logging
|
import logging
|
||||||
import ../protocol/wire
|
import ../protocol/wire
|
||||||
@@ -37,6 +38,7 @@ type
|
|||||||
gossipProtocol*: GossipProtocol
|
gossipProtocol*: GossipProtocol
|
||||||
tls*: TLSContext
|
tls*: TLSContext
|
||||||
activeConnections*: int
|
activeConnections*: int
|
||||||
|
activeConnectionsLock*: Lock
|
||||||
|
|
||||||
proc newServer*(config: BaraConfig): Server =
|
proc newServer*(config: BaraConfig): Server =
|
||||||
let dataDir = config.dataDir / "server"
|
let dataDir = config.dataDir / "server"
|
||||||
@@ -83,13 +85,14 @@ proc newServer*(config: BaraConfig): Server =
|
|||||||
gp.onSuspect = proc(nodeId: string) {.gcsafe.} =
|
gp.onSuspect = proc(nodeId: string) {.gcsafe.} =
|
||||||
cm.onNodeSuspect(nodeId)
|
cm.onNodeSuspect(nodeId)
|
||||||
|
|
||||||
Server(config: config, running: false, db: db, ctx: ctx,
|
result = Server(config: config, running: false, db: db, ctx: ctx,
|
||||||
txnManager: ctx.txnManager, distTxnManager: newDistTxnManager(),
|
txnManager: ctx.txnManager, distTxnManager: newDistTxnManager(),
|
||||||
replicationManager: newReplicationManager(),
|
replicationManager: newReplicationManager(),
|
||||||
shardRouter: shardRouter,
|
shardRouter: shardRouter,
|
||||||
clusterMembership: cm,
|
clusterMembership: cm,
|
||||||
gossipProtocol: gp,
|
gossipProtocol: gp,
|
||||||
tls: tls)
|
tls: tls)
|
||||||
|
initLock(result.activeConnectionsLock)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Wire Protocol Helpers
|
# Wire Protocol Helpers
|
||||||
@@ -105,9 +108,13 @@ proc parseHeader(data: string): (bool, MessageHeader) =
|
|||||||
if data.len < 12:
|
if data.len < 12:
|
||||||
return (false, MessageHeader())
|
return (false, MessageHeader())
|
||||||
let rawKind = readUint32BE(data, 0)
|
let rawKind = readUint32BE(data, 0)
|
||||||
{.push warning[HoleEnumConv]: off.}
|
let kind = cast[MsgKind](rawKind)
|
||||||
let kind = MsgKind(rawKind)
|
case kind
|
||||||
{.pop.}
|
of mkClientHandshake, mkQuery, mkQueryParams, mkExecute, mkBatch, mkTransaction, mkClose, mkPing, mkAuth,
|
||||||
|
mkServerHandshake, mkReady, mkData, mkComplete, mkError, mkAuthChallenge, mkAuthOk, mkSchemaChange, mkPong, mkTransactionState:
|
||||||
|
discard
|
||||||
|
else:
|
||||||
|
return (false, MessageHeader())
|
||||||
let length = readUint32BE(data, 4)
|
let length = readUint32BE(data, 4)
|
||||||
let requestId = readUint32BE(data, 8)
|
let requestId = readUint32BE(data, 8)
|
||||||
return (true, MessageHeader(kind: kind, length: length, requestId: requestId))
|
return (true, MessageHeader(kind: kind, length: length, requestId: requestId))
|
||||||
@@ -523,8 +530,12 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
errorMsg("Client " & $clientId & " error: " & e.msg)
|
errorMsg("Client " & $clientId & " error: " & e.msg)
|
||||||
finally:
|
finally:
|
||||||
|
acquire(server.activeConnectionsLock)
|
||||||
|
try:
|
||||||
if server.activeConnections > 0:
|
if server.activeConnections > 0:
|
||||||
dec server.activeConnections
|
dec server.activeConnections
|
||||||
|
finally:
|
||||||
|
release(server.activeConnectionsLock)
|
||||||
info("Client " & $clientId & " disconnected")
|
info("Client " & $clientId & " disconnected")
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
@@ -547,7 +558,16 @@ proc run*(server: Server) {.async.} =
|
|||||||
info("BaraDB listening on " & server.config.address & ":" & $server.config.port)
|
info("BaraDB listening on " & server.config.address & ":" & $server.config.port)
|
||||||
while server.running:
|
while server.running:
|
||||||
let client = await sock.accept()
|
let client = await sock.accept()
|
||||||
|
acquire(server.activeConnectionsLock)
|
||||||
|
var shouldAccept = true
|
||||||
|
try:
|
||||||
if server.config.maxConnections > 0 and server.activeConnections >= server.config.maxConnections:
|
if server.config.maxConnections > 0 and server.activeConnections >= server.config.maxConnections:
|
||||||
|
shouldAccept = false
|
||||||
|
else:
|
||||||
|
inc server.activeConnections
|
||||||
|
finally:
|
||||||
|
release(server.activeConnectionsLock)
|
||||||
|
if not shouldAccept:
|
||||||
client.close()
|
client.close()
|
||||||
continue
|
continue
|
||||||
if server.tls != nil:
|
if server.tls != nil:
|
||||||
@@ -555,10 +575,15 @@ proc run*(server: Server) {.async.} =
|
|||||||
server.tls.wrapServer(client)
|
server.tls.wrapServer(client)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errorMsg("TLS handshake failed: " & e.msg)
|
errorMsg("TLS handshake failed: " & e.msg)
|
||||||
|
acquire(server.activeConnectionsLock)
|
||||||
|
try:
|
||||||
|
if server.activeConnections > 0:
|
||||||
|
dec server.activeConnections
|
||||||
|
finally:
|
||||||
|
release(server.activeConnectionsLock)
|
||||||
client.close()
|
client.close()
|
||||||
continue
|
continue
|
||||||
inc clientId
|
inc clientId
|
||||||
inc server.activeConnections
|
|
||||||
asyncCheck server.handleClient(client, clientId)
|
asyncCheck server.handleClient(client, clientId)
|
||||||
|
|
||||||
proc stop*(server: Server) =
|
proc stop*(server: Server) =
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import std/strutils
|
|||||||
import std/unicode
|
import std/unicode
|
||||||
import std/math
|
import std/math
|
||||||
import std/algorithm
|
import std/algorithm
|
||||||
|
import std/locks
|
||||||
|
|
||||||
type
|
type
|
||||||
TermFreq* = Table[string, int]
|
TermFreq* = Table[string, int]
|
||||||
@@ -20,6 +21,7 @@ type
|
|||||||
docCount*: int
|
docCount*: int
|
||||||
avgDocLen*: float64
|
avgDocLen*: float64
|
||||||
totalTerms*: int
|
totalTerms*: int
|
||||||
|
lock*: Lock
|
||||||
|
|
||||||
SearchResult* = object
|
SearchResult* = object
|
||||||
docId*: uint64
|
docId*: uint64
|
||||||
@@ -111,16 +113,19 @@ proc tokenize*(text: string, config: TokenizerConfig = defaultTokenizerConfig())
|
|||||||
result.add(token)
|
result.add(token)
|
||||||
|
|
||||||
proc newInvertedIndex*(): InvertedIndex =
|
proc newInvertedIndex*(): InvertedIndex =
|
||||||
InvertedIndex(
|
result = InvertedIndex(
|
||||||
postings: initTable[string, seq[PostingEntry]](),
|
postings: initTable[string, seq[PostingEntry]](),
|
||||||
docLengths: initTable[uint64, int](),
|
docLengths: initTable[uint64, int](),
|
||||||
docCount: 0,
|
docCount: 0,
|
||||||
avgDocLen: 0.0,
|
avgDocLen: 0.0,
|
||||||
totalTerms: 0,
|
totalTerms: 0,
|
||||||
)
|
)
|
||||||
|
initLock(result.lock)
|
||||||
|
|
||||||
proc addDocument*(idx: InvertedIndex, docId: uint64, text: string,
|
proc addDocument*(idx: InvertedIndex, docId: uint64, text: string,
|
||||||
config: TokenizerConfig = defaultTokenizerConfig()) =
|
config: TokenizerConfig = defaultTokenizerConfig()) =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
let tokens = tokenize(text, config)
|
let tokens = tokenize(text, config)
|
||||||
var termFreqs = initTable[string, int]()
|
var termFreqs = initTable[string, int]()
|
||||||
var positions = initTable[string, seq[int]]()
|
var positions = initTable[string, seq[int]]()
|
||||||
@@ -145,8 +150,12 @@ proc addDocument*(idx: InvertedIndex, docId: uint64, text: string,
|
|||||||
inc idx.docCount
|
inc idx.docCount
|
||||||
idx.totalTerms += tokens.len
|
idx.totalTerms += tokens.len
|
||||||
idx.avgDocLen = float64(idx.totalTerms) / float64(idx.docCount)
|
idx.avgDocLen = float64(idx.totalTerms) / float64(idx.docCount)
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
proc removeDocument*(idx: InvertedIndex, docId: uint64) =
|
proc removeDocument*(idx: InvertedIndex, docId: uint64) =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
if docId notin idx.docLengths:
|
if docId notin idx.docLengths:
|
||||||
return
|
return
|
||||||
let docLen = idx.docLengths[docId]
|
let docLen = idx.docLengths[docId]
|
||||||
@@ -162,8 +171,10 @@ proc removeDocument*(idx: InvertedIndex, docId: uint64) =
|
|||||||
if entry.docId != docId:
|
if entry.docId != docId:
|
||||||
newPostings.add(entry)
|
newPostings.add(entry)
|
||||||
postings = newPostings
|
postings = newPostings
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
proc bm25Score*(idx: InvertedIndex, term: string, docId: uint64,
|
proc bm25ScoreUnsafe(idx: InvertedIndex, term: string, docId: uint64,
|
||||||
k1: float64 = 1.2, b: float64 = 0.75): float64 =
|
k1: float64 = 1.2, b: float64 = 0.75): float64 =
|
||||||
if term notin idx.postings:
|
if term notin idx.postings:
|
||||||
return 0.0
|
return 0.0
|
||||||
@@ -190,8 +201,18 @@ proc bm25Score*(idx: InvertedIndex, term: string, docId: uint64,
|
|||||||
(float64(tf) + k1 * (1.0 - b + b * docLen / idx.avgDocLen))
|
(float64(tf) + k1 * (1.0 - b + b * docLen / idx.avgDocLen))
|
||||||
return idf * tfNorm
|
return idf * tfNorm
|
||||||
|
|
||||||
|
proc bm25Score*(idx: InvertedIndex, term: string, docId: uint64,
|
||||||
|
k1: float64 = 1.2, b: float64 = 0.75): float64 =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
|
result = bm25ScoreUnsafe(idx, term, docId, k1, b)
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
proc search*(idx: InvertedIndex, query: string, limit: int = 10,
|
proc search*(idx: InvertedIndex, query: string, limit: int = 10,
|
||||||
config: TokenizerConfig = defaultTokenizerConfig()): seq[SearchResult] =
|
config: TokenizerConfig = defaultTokenizerConfig()): seq[SearchResult] =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
let queryTokens = tokenize(query, config)
|
let queryTokens = tokenize(query, config)
|
||||||
if queryTokens.len == 0:
|
if queryTokens.len == 0:
|
||||||
return @[]
|
return @[]
|
||||||
@@ -203,7 +224,7 @@ proc search*(idx: InvertedIndex, query: string, limit: int = 10,
|
|||||||
if token notin idx.postings:
|
if token notin idx.postings:
|
||||||
continue
|
continue
|
||||||
for entry in idx.postings[token]:
|
for entry in idx.postings[token]:
|
||||||
let score = bm25Score(idx, token, entry.docId)
|
let score = bm25ScoreUnsafe(idx, token, entry.docId)
|
||||||
if entry.docId notin docScores:
|
if entry.docId notin docScores:
|
||||||
docScores[entry.docId] = 0.0
|
docScores[entry.docId] = 0.0
|
||||||
docHighlights[entry.docId] = @[]
|
docHighlights[entry.docId] = @[]
|
||||||
@@ -227,12 +248,25 @@ proc search*(idx: InvertedIndex, query: string, limit: int = 10,
|
|||||||
results = results[0..<limit]
|
results = results[0..<limit]
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
proc termCount*(idx: InvertedIndex): int = idx.postings.len
|
proc termCount*(idx: InvertedIndex): int =
|
||||||
proc documentCount*(idx: InvertedIndex): int = idx.docCount
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
|
result = idx.postings.len
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
|
proc documentCount*(idx: InvertedIndex): int =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
|
result = idx.docCount
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
# TF-IDF ranking
|
# TF-IDF ranking
|
||||||
proc tfidfScore*(idx: InvertedIndex, term: string, docId: uint64): float64 =
|
proc tfidfScoreUnsafe(idx: InvertedIndex, term: string, docId: uint64): float64 =
|
||||||
if term notin idx.postings:
|
if term notin idx.postings:
|
||||||
return 0.0
|
return 0.0
|
||||||
let df = idx.postings[term].len
|
let df = idx.postings[term].len
|
||||||
@@ -249,8 +283,17 @@ proc tfidfScore*(idx: InvertedIndex, term: string, docId: uint64): float64 =
|
|||||||
let idf = ln(float64(n) / float64(df))
|
let idf = ln(float64(n) / float64(df))
|
||||||
return float64(tf) * idf
|
return float64(tf) * idf
|
||||||
|
|
||||||
|
proc tfidfScore*(idx: InvertedIndex, term: string, docId: uint64): float64 =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
|
result = tfidfScoreUnsafe(idx, term, docId)
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
proc searchTfidf*(idx: InvertedIndex, query: string, limit: int = 10,
|
proc searchTfidf*(idx: InvertedIndex, query: string, limit: int = 10,
|
||||||
config: TokenizerConfig = defaultTokenizerConfig()): seq[SearchResult] =
|
config: TokenizerConfig = defaultTokenizerConfig()): seq[SearchResult] =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
let queryTokens = tokenize(query, config)
|
let queryTokens = tokenize(query, config)
|
||||||
if queryTokens.len == 0:
|
if queryTokens.len == 0:
|
||||||
return @[]
|
return @[]
|
||||||
@@ -261,7 +304,7 @@ proc searchTfidf*(idx: InvertedIndex, query: string, limit: int = 10,
|
|||||||
if token notin idx.postings:
|
if token notin idx.postings:
|
||||||
continue
|
continue
|
||||||
for entry in idx.postings[token]:
|
for entry in idx.postings[token]:
|
||||||
let score = idx.tfidfScore(token, entry.docId)
|
let score = tfidfScoreUnsafe(idx, token, entry.docId)
|
||||||
if entry.docId notin docScores:
|
if entry.docId notin docScores:
|
||||||
docScores[entry.docId] = 0.0
|
docScores[entry.docId] = 0.0
|
||||||
docScores[entry.docId] += score
|
docScores[entry.docId] += score
|
||||||
@@ -274,6 +317,8 @@ proc searchTfidf*(idx: InvertedIndex, query: string, limit: int = 10,
|
|||||||
if results.len > limit:
|
if results.len > limit:
|
||||||
results = results[0..<limit]
|
results = results[0..<limit]
|
||||||
return results
|
return results
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
# Levenshtein distance for fuzzy matching
|
# Levenshtein distance for fuzzy matching
|
||||||
proc levenshtein*(a, b: string): int =
|
proc levenshtein*(a, b: string): int =
|
||||||
@@ -295,6 +340,8 @@ proc levenshtein*(a, b: string): int =
|
|||||||
|
|
||||||
proc fuzzySearch*(idx: InvertedIndex, query: string, maxDistance: int = 2,
|
proc fuzzySearch*(idx: InvertedIndex, query: string, maxDistance: int = 2,
|
||||||
limit: int = 10, config: TokenizerConfig = defaultTokenizerConfig()): seq[SearchResult] =
|
limit: int = 10, config: TokenizerConfig = defaultTokenizerConfig()): seq[SearchResult] =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
let queryTokens = tokenize(query, config)
|
let queryTokens = tokenize(query, config)
|
||||||
if queryTokens.len == 0:
|
if queryTokens.len == 0:
|
||||||
return @[]
|
return @[]
|
||||||
@@ -319,10 +366,14 @@ proc fuzzySearch*(idx: InvertedIndex, query: string, maxDistance: int = 2,
|
|||||||
if results.len > limit:
|
if results.len > limit:
|
||||||
results = results[0..<limit]
|
results = results[0..<limit]
|
||||||
return results
|
return results
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|
||||||
# Regex search
|
# Regex search
|
||||||
proc regexSearch*(idx: InvertedIndex, pattern: string,
|
proc regexSearch*(idx: InvertedIndex, pattern: string,
|
||||||
limit: int = 10): seq[SearchResult] =
|
limit: int = 10): seq[SearchResult] =
|
||||||
|
acquire(idx.lock)
|
||||||
|
try:
|
||||||
var docScores = initTable[uint64, float64]()
|
var docScores = initTable[uint64, float64]()
|
||||||
|
|
||||||
for term in idx.postings.keys:
|
for term in idx.postings.keys:
|
||||||
@@ -360,3 +411,5 @@ proc regexSearch*(idx: InvertedIndex, pattern: string,
|
|||||||
if results.len > limit:
|
if results.len > limit:
|
||||||
results = results[0..<limit]
|
results = results[0..<limit]
|
||||||
return results
|
return results
|
||||||
|
finally:
|
||||||
|
release(idx.lock)
|
||||||
|
|||||||
@@ -205,7 +205,10 @@ proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): Wire
|
|||||||
raise newException(ValueError, "Wire protocol: max deserialization depth exceeded")
|
raise newException(ValueError, "Wire protocol: max deserialization depth exceeded")
|
||||||
if pos >= buf.len:
|
if pos >= buf.len:
|
||||||
raise newException(ValueError, "Wire protocol: unexpected end of buffer")
|
raise newException(ValueError, "Wire protocol: unexpected end of buffer")
|
||||||
let kind = FieldKind(buf[pos])
|
let kindByte = buf[pos]
|
||||||
|
if kindByte > byte(high(FieldKind)):
|
||||||
|
raise newException(ValueError, "Wire protocol: invalid field kind")
|
||||||
|
let kind = FieldKind(kindByte)
|
||||||
inc pos
|
inc pos
|
||||||
case kind
|
case kind
|
||||||
of fkNull: result = WireValue(kind: fkNull)
|
of fkNull: result = WireValue(kind: fkNull)
|
||||||
@@ -223,26 +226,18 @@ proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): Wire
|
|||||||
pos += 2
|
pos += 2
|
||||||
result = WireValue(kind: fkInt16, int16Val: val)
|
result = WireValue(kind: fkInt16, int16Val: val)
|
||||||
of fkInt32:
|
of fkInt32:
|
||||||
result = WireValue(kind: fkInt32, int32Val: int32(readUint32(buf, pos)))
|
result = WireValue(kind: fkInt32, int32Val: cast[int32](readUint32(buf, pos)))
|
||||||
of fkInt64:
|
of fkInt64:
|
||||||
result = WireValue(kind: fkInt64, int64Val: int64(readUint64(buf, pos)))
|
result = WireValue(kind: fkInt64, int64Val: cast[int64](readUint64(buf, pos)))
|
||||||
of fkFloat32:
|
of fkFloat32:
|
||||||
var fl: float32
|
var fl: float32
|
||||||
var bytes: array[4, byte]
|
let raw = readUint32(buf, pos)
|
||||||
for i in 0..3: bytes[i] = buf[pos + i]
|
fl = cast[float32](raw)
|
||||||
var i32: int32
|
|
||||||
bigEndian32(addr i32, unsafeAddr bytes)
|
|
||||||
fl = cast[float32](i32)
|
|
||||||
pos += 4
|
|
||||||
result = WireValue(kind: fkFloat32, float32Val: fl)
|
result = WireValue(kind: fkFloat32, float32Val: fl)
|
||||||
of fkFloat64:
|
of fkFloat64:
|
||||||
var fl: float64
|
var fl: float64
|
||||||
var bytes: array[8, byte]
|
let raw = readUint64(buf, pos)
|
||||||
for i in 0..7: bytes[i] = buf[pos + i]
|
fl = cast[float64](raw)
|
||||||
var i64: int64
|
|
||||||
bigEndian64(addr i64, unsafeAddr bytes)
|
|
||||||
fl = cast[float64](i64)
|
|
||||||
pos += 8
|
|
||||||
result = WireValue(kind: fkFloat64, float64Val: fl)
|
result = WireValue(kind: fkFloat64, float64Val: fl)
|
||||||
of fkString:
|
of fkString:
|
||||||
result = WireValue(kind: fkString, strVal: readString(buf, pos))
|
result = WireValue(kind: fkString, strVal: readString(buf, pos))
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import std/times
|
|||||||
import std/json
|
import std/json
|
||||||
import std/random
|
import std/random
|
||||||
import std/monotimes
|
import std/monotimes
|
||||||
|
import std/locks
|
||||||
import lexer as qlex
|
import lexer as qlex
|
||||||
import parser as qpar
|
import parser as qpar
|
||||||
import ast
|
import ast
|
||||||
@@ -74,6 +75,7 @@ type
|
|||||||
sessionVars*: Table[string, string] # session-scoped key/value variables
|
sessionVars*: Table[string, string] # session-scoped key/value variables
|
||||||
autoIncCounters*: Table[string, int64] # table.col -> next auto-increment value
|
autoIncCounters*: Table[string, int64] # table.col -> next auto-increment value
|
||||||
sequences*: Table[string, int64] # sequence name -> current value
|
sequences*: Table[string, int64] # sequence name -> current value
|
||||||
|
ctxLock*: Lock # protects tables, views, btrees, ftsIndexes, users, policies, autoIncCounters, sequences
|
||||||
|
|
||||||
MigrationRecord* = object
|
MigrationRecord* = object
|
||||||
name*: string
|
name*: string
|
||||||
@@ -160,6 +162,7 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
|||||||
autoIncCounters: initTable[string, int64](),
|
autoIncCounters: initTable[string, int64](),
|
||||||
sequences: initTable[string, int64](),
|
sequences: initTable[string, int64](),
|
||||||
onChange: nil)
|
onChange: nil)
|
||||||
|
initLock(result.ctxLock)
|
||||||
restoreSchema(result)
|
restoreSchema(result)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -332,7 +335,10 @@ proc restoreSchema(ctx: ExecutionContext) =
|
|||||||
else: discard
|
else: discard
|
||||||
|
|
||||||
proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
||||||
ExecutionContext(db: ctx.db, tables: ctx.tables,
|
var svCopy = initTable[string, string]()
|
||||||
|
for k, v in ctx.sessionVars:
|
||||||
|
svCopy[k] = v
|
||||||
|
result = ExecutionContext(db: ctx.db, tables: ctx.tables,
|
||||||
btrees: ctx.btrees, views: ctx.views,
|
btrees: ctx.btrees, views: ctx.views,
|
||||||
cteTables: initTable[string, seq[Row]](),
|
cteTables: initTable[string, seq[Row]](),
|
||||||
ftsIndexes: ctx.ftsIndexes,
|
ftsIndexes: ctx.ftsIndexes,
|
||||||
@@ -340,10 +346,11 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
|||||||
users: ctx.users, policies: ctx.policies,
|
users: ctx.users, policies: ctx.policies,
|
||||||
txnManager: ctx.txnManager,
|
txnManager: ctx.txnManager,
|
||||||
currentUser: ctx.currentUser, currentRole: ctx.currentRole,
|
currentUser: ctx.currentUser, currentRole: ctx.currentRole,
|
||||||
sessionVars: ctx.sessionVars,
|
sessionVars: svCopy,
|
||||||
autoIncCounters: ctx.autoIncCounters,
|
autoIncCounters: ctx.autoIncCounters,
|
||||||
sequences: ctx.sequences,
|
sequences: ctx.sequences,
|
||||||
pendingTxn: nil, onChange: ctx.onChange)
|
pendingTxn: nil, onChange: ctx.onChange)
|
||||||
|
initLock(result.ctxLock)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Migration Helpers
|
# Migration Helpers
|
||||||
@@ -1059,18 +1066,27 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
if ctx == nil: return "0"
|
if ctx == nil: return "0"
|
||||||
let seqName = evalExpr(expr.irFuncArgs[0], row, ctx)
|
let seqName = evalExpr(expr.irFuncArgs[0], row, ctx)
|
||||||
var val: int64 = 0
|
var val: int64 = 0
|
||||||
|
acquire(ctx.ctxLock)
|
||||||
|
try:
|
||||||
if seqName in ctx.sequences:
|
if seqName in ctx.sequences:
|
||||||
val = ctx.sequences[seqName]
|
val = ctx.sequences[seqName]
|
||||||
val += 1
|
val += 1
|
||||||
ctx.sequences[seqName] = val
|
ctx.sequences[seqName] = val
|
||||||
|
finally:
|
||||||
|
release(ctx.ctxLock)
|
||||||
return $val
|
return $val
|
||||||
of "currval":
|
of "currval":
|
||||||
if expr.irFuncArgs.len < 1:
|
if expr.irFuncArgs.len < 1:
|
||||||
return "0"
|
return "0"
|
||||||
if ctx == nil: return "0"
|
if ctx == nil: return "0"
|
||||||
let seqName = evalExpr(expr.irFuncArgs[0], row, ctx)
|
let seqName = evalExpr(expr.irFuncArgs[0], row, ctx)
|
||||||
|
acquire(ctx.ctxLock)
|
||||||
|
try:
|
||||||
if seqName in ctx.sequences:
|
if seqName in ctx.sequences:
|
||||||
return $ctx.sequences[seqName]
|
return $ctx.sequences[seqName]
|
||||||
|
finally:
|
||||||
|
release(ctx.ctxLock)
|
||||||
|
return "0"
|
||||||
return "0"
|
return "0"
|
||||||
of "snowflake_id":
|
of "snowflake_id":
|
||||||
# Snowflake ID: timestamp_ms(41 bits) | node_id(10 bits) | sequence(12 bits)
|
# Snowflake ID: timestamp_ms(41 bits) | node_id(10 bits) | sequence(12 bits)
|
||||||
@@ -1457,6 +1473,7 @@ proc validateType*(colType: string, value: string): (bool, string) =
|
|||||||
return (false, "Vector dimension mismatch: expected " & $expectedDim & " but got " & $vec.len)
|
return (false, "Vector dimension mismatch: expected " & $expectedDim & " but got " & $vec.len)
|
||||||
return (true, "")
|
return (true, "")
|
||||||
|
|
||||||
|
proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult
|
||||||
proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult
|
proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult
|
||||||
proc executeMigrationSql(ctx: ExecutionContext, sql: string): ExecResult
|
proc executeMigrationSql(ctx: ExecutionContext, sql: string): ExecResult
|
||||||
|
|
||||||
@@ -1468,7 +1485,7 @@ proc fireTriggers*(ctx: ExecutionContext, tableName: string, timing: string, eve
|
|||||||
let tokens = qlex.tokenize(trig.action.strVal)
|
let tokens = qlex.tokenize(trig.action.strVal)
|
||||||
let astNode = qpar.parse(tokens)
|
let astNode = qpar.parse(tokens)
|
||||||
if astNode.stmts.len > 0:
|
if astNode.stmts.len > 0:
|
||||||
discard executeQuery(ctx, astNode)
|
discard executeQueryImpl(ctx, astNode)
|
||||||
|
|
||||||
proc validateConstraints*(ctx: ExecutionContext, tableName: string,
|
proc validateConstraints*(ctx: ExecutionContext, tableName: string,
|
||||||
fields: seq[string], values: seq[seq[string]], skipPkCheck: bool = false): (bool, string) =
|
fields: seq[string], values: seq[seq[string]], skipPkCheck: bool = false): (bool, string) =
|
||||||
@@ -3075,7 +3092,21 @@ proc getSelectColumns(stmt: Node): seq[string] =
|
|||||||
else:
|
else:
|
||||||
result.add("col" & $i)
|
result.add("col" & $i)
|
||||||
|
|
||||||
proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult =
|
proc isDDL(stmt: Node): bool =
|
||||||
|
case stmt.kind
|
||||||
|
of nkCreateTable, nkDropTable, nkAlterTable,
|
||||||
|
nkCreateView, nkDropView,
|
||||||
|
nkCreateIndex, nkDropIndex,
|
||||||
|
nkCreateTrigger, nkDropTrigger,
|
||||||
|
nkCreateUser, nkDropUser,
|
||||||
|
nkCreatePolicy, nkDropPolicy,
|
||||||
|
nkGrant, nkRevoke,
|
||||||
|
nkEnableRLS, nkDisableRLS:
|
||||||
|
result = true
|
||||||
|
else:
|
||||||
|
result = false
|
||||||
|
|
||||||
|
proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult =
|
||||||
if astNode == nil or astNode.stmts.len == 0:
|
if astNode == nil or astNode.stmts.len == 0:
|
||||||
return okResult()
|
return okResult()
|
||||||
|
|
||||||
@@ -3109,7 +3140,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
# Step 1: Execute the non-recursive anchor (left side of UNION)
|
# Step 1: Execute the non-recursive anchor (left side of UNION)
|
||||||
var innerLeft = Node(kind: nkStatementList, stmts: @[])
|
var innerLeft = Node(kind: nkStatementList, stmts: @[])
|
||||||
innerLeft.stmts.add(cteQuery.setOpLeft)
|
innerLeft.stmts.add(cteQuery.setOpLeft)
|
||||||
let anchorRes = executeQuery(ctx, innerLeft)
|
let anchorRes = executeQueryImpl(ctx, innerLeft)
|
||||||
for row in anchorRes.rows:
|
for row in anchorRes.rows:
|
||||||
allRows.add(row)
|
allRows.add(row)
|
||||||
|
|
||||||
@@ -3125,7 +3156,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
|
|
||||||
var innerRight = Node(kind: nkStatementList, stmts: @[])
|
var innerRight = Node(kind: nkStatementList, stmts: @[])
|
||||||
innerRight.stmts.add(cteQuery.setOpRight)
|
innerRight.stmts.add(cteQuery.setOpRight)
|
||||||
let rightRes = executeQuery(ctx, innerRight)
|
let rightRes = executeQueryImpl(ctx, innerRight)
|
||||||
|
|
||||||
ctx.cteTables = savedCte
|
ctx.cteTables = savedCte
|
||||||
|
|
||||||
@@ -3159,7 +3190,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
# Recursive CTE without UNION — treat as non-recursive fallback
|
# Recursive CTE without UNION — treat as non-recursive fallback
|
||||||
var inner = Node(kind: nkStatementList, stmts: @[])
|
var inner = Node(kind: nkStatementList, stmts: @[])
|
||||||
inner.stmts.add(cteQuery)
|
inner.stmts.add(cteQuery)
|
||||||
let cteRes = executeQuery(ctx, inner)
|
let cteRes = executeQueryImpl(ctx, inner)
|
||||||
var cteRows: seq[Row] = @[]
|
var cteRows: seq[Row] = @[]
|
||||||
for row in cteRes.rows:
|
for row in cteRes.rows:
|
||||||
cteRows.add(row)
|
cteRows.add(row)
|
||||||
@@ -3167,7 +3198,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
else:
|
else:
|
||||||
var inner = Node(kind: nkStatementList, stmts: @[])
|
var inner = Node(kind: nkStatementList, stmts: @[])
|
||||||
inner.stmts.add(cteQuery)
|
inner.stmts.add(cteQuery)
|
||||||
let cteRes = executeQuery(ctx, inner)
|
let cteRes = executeQueryImpl(ctx, inner)
|
||||||
var cteRows: seq[Row] = @[]
|
var cteRows: seq[Row] = @[]
|
||||||
for row in cteRes.rows:
|
for row in cteRes.rows:
|
||||||
cteRows.add(row)
|
cteRows.add(row)
|
||||||
@@ -3180,7 +3211,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
# Execute the view's underlying query
|
# Execute the view's underlying query
|
||||||
var inner = Node(kind: nkStatementList, stmts: @[])
|
var inner = Node(kind: nkStatementList, stmts: @[])
|
||||||
inner.stmts.add(viewQuery)
|
inner.stmts.add(viewQuery)
|
||||||
let innerResult = executeQuery(ctx, inner)
|
let innerResult = executeQueryImpl(ctx, inner)
|
||||||
# Now filter and project with outer query constraints
|
# Now filter and project with outer query constraints
|
||||||
var filteredRows = innerResult.rows
|
var filteredRows = innerResult.rows
|
||||||
var cols = innerResult.columns
|
var cols = innerResult.columns
|
||||||
@@ -3405,11 +3436,11 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
# Execute left and right queries
|
# Execute left and right queries
|
||||||
var innerLeft = Node(kind: nkStatementList, stmts: @[])
|
var innerLeft = Node(kind: nkStatementList, stmts: @[])
|
||||||
innerLeft.stmts.add(stmt.setOpLeft)
|
innerLeft.stmts.add(stmt.setOpLeft)
|
||||||
let leftRes = executeQuery(ctx, innerLeft)
|
let leftRes = executeQueryImpl(ctx, innerLeft)
|
||||||
|
|
||||||
var innerRight = Node(kind: nkStatementList, stmts: @[])
|
var innerRight = Node(kind: nkStatementList, stmts: @[])
|
||||||
innerRight.stmts.add(stmt.setOpRight)
|
innerRight.stmts.add(stmt.setOpRight)
|
||||||
let rightRes = executeQuery(ctx, innerRight)
|
let rightRes = executeQueryImpl(ctx, innerRight)
|
||||||
|
|
||||||
# Derive columns from left side
|
# Derive columns from left side
|
||||||
var cols = leftRes.columns
|
var cols = leftRes.columns
|
||||||
@@ -3493,9 +3524,13 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
if col.autoIncrement and col.name notin mutableFields:
|
if col.autoIncrement and col.name notin mutableFields:
|
||||||
let counterKey = stmt.insTarget & "." & col.name
|
let counterKey = stmt.insTarget & "." & col.name
|
||||||
var nextVal: int64 = 1
|
var nextVal: int64 = 1
|
||||||
|
acquire(ctx.ctxLock)
|
||||||
|
try:
|
||||||
if counterKey in ctx.autoIncCounters:
|
if counterKey in ctx.autoIncCounters:
|
||||||
nextVal = ctx.autoIncCounters[counterKey]
|
nextVal = ctx.autoIncCounters[counterKey]
|
||||||
ctx.autoIncCounters[counterKey] = nextVal + int64(mutableValues.len)
|
ctx.autoIncCounters[counterKey] = nextVal + int64(mutableValues.len)
|
||||||
|
finally:
|
||||||
|
release(ctx.ctxLock)
|
||||||
# Insert at position 0 so it becomes the primary storage key
|
# Insert at position 0 so it becomes the primary storage key
|
||||||
mutableFields.insert(col.name, 0)
|
mutableFields.insert(col.name, 0)
|
||||||
for i in 0..<mutableValues.len:
|
for i in 0..<mutableValues.len:
|
||||||
@@ -3510,8 +3545,12 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
try:
|
try:
|
||||||
let intVal = parseInt(providedVal)
|
let intVal = parseInt(providedVal)
|
||||||
let counterKey = stmt.insTarget & "." & col.name
|
let counterKey = stmt.insTarget & "." & col.name
|
||||||
|
acquire(ctx.ctxLock)
|
||||||
|
try:
|
||||||
if counterKey notin ctx.autoIncCounters or intVal >= ctx.autoIncCounters[counterKey]:
|
if counterKey notin ctx.autoIncCounters or intVal >= ctx.autoIncCounters[counterKey]:
|
||||||
ctx.autoIncCounters[counterKey] = intVal + 1
|
ctx.autoIncCounters[counterKey] = intVal + 1
|
||||||
|
finally:
|
||||||
|
release(ctx.ctxLock)
|
||||||
except: discard
|
except: discard
|
||||||
|
|
||||||
applyDefaultValues(tbl, mutableFields, mutableValues)
|
applyDefaultValues(tbl, mutableFields, mutableValues)
|
||||||
@@ -3657,7 +3696,7 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
var sourceRows: seq[Row] = @[]
|
var sourceRows: seq[Row] = @[]
|
||||||
if stmt.mergeSource != nil:
|
if stmt.mergeSource != nil:
|
||||||
if stmt.mergeSource.kind == nkSelect:
|
if stmt.mergeSource.kind == nkSelect:
|
||||||
let srcRes = executeQuery(ctx, Node(kind: nkStatementList, stmts: @[stmt.mergeSource]))
|
let srcRes = executeQueryImpl(ctx, Node(kind: nkStatementList, stmts: @[stmt.mergeSource]))
|
||||||
sourceRows = srcRes.rows
|
sourceRows = srcRes.rows
|
||||||
elif stmt.mergeSource.kind == nkIdent:
|
elif stmt.mergeSource.kind == nkIdent:
|
||||||
sourceRows = execScan(ctx, stmt.mergeSource.identName)
|
sourceRows = execScan(ctx, stmt.mergeSource.identName)
|
||||||
@@ -4236,9 +4275,22 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
return errResult("Unsupported statement type: " & $stmt.kind)
|
return errResult("Unsupported statement type: " & $stmt.kind)
|
||||||
|
|
||||||
|
|
||||||
|
proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult =
|
||||||
|
if astNode == nil or astNode.stmts.len == 0:
|
||||||
|
return okResult()
|
||||||
|
let stmt = astNode.stmts[0]
|
||||||
|
if isDDL(stmt):
|
||||||
|
acquire(ctx.ctxLock)
|
||||||
|
try:
|
||||||
|
result = executeQueryImpl(ctx, astNode, params)
|
||||||
|
finally:
|
||||||
|
release(ctx.ctxLock)
|
||||||
|
else:
|
||||||
|
result = executeQueryImpl(ctx, astNode, params)
|
||||||
|
|
||||||
proc executeMigrationSql(ctx: ExecutionContext, sql: string): ExecResult =
|
proc executeMigrationSql(ctx: ExecutionContext, sql: string): ExecResult =
|
||||||
let tokens = qlex.tokenize(sql)
|
let tokens = qlex.tokenize(sql)
|
||||||
let astNode = qpar.parse(tokens)
|
let astNode = qpar.parse(tokens)
|
||||||
if astNode.stmts.len > 0:
|
if astNode.stmts.len > 0:
|
||||||
return executeQuery(ctx, astNode)
|
return executeQueryImpl(ctx, astNode)
|
||||||
return okResult(msg="Empty migration body")
|
return okResult(msg="Empty migration body")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
## B-Tree Index — ordered key-value index
|
## B-Tree Index — ordered key-value index
|
||||||
import std/tables
|
import std/tables
|
||||||
|
import std/locks
|
||||||
|
|
||||||
const
|
const
|
||||||
DefaultBTreeOrder* = 32
|
DefaultBTreeOrder* = 32
|
||||||
@@ -16,6 +17,7 @@ type
|
|||||||
root: BTreeNode[K, V]
|
root: BTreeNode[K, V]
|
||||||
order: int
|
order: int
|
||||||
size: int
|
size: int
|
||||||
|
lock*: Lock
|
||||||
|
|
||||||
proc newBTreeNode[K, V](isLeaf: bool = true): BTreeNode[K, V] =
|
proc newBTreeNode[K, V](isLeaf: bool = true): BTreeNode[K, V] =
|
||||||
BTreeNode[K, V](
|
BTreeNode[K, V](
|
||||||
@@ -24,7 +26,8 @@ proc newBTreeNode[K, V](isLeaf: bool = true): BTreeNode[K, V] =
|
|||||||
)
|
)
|
||||||
|
|
||||||
proc newBTreeIndex*[K, V](order: int = DefaultBTreeOrder): BTreeIndex[K, V] =
|
proc newBTreeIndex*[K, V](order: int = DefaultBTreeOrder): BTreeIndex[K, V] =
|
||||||
BTreeIndex[K, V](root: newBTreeNode[K, V](), order: order, size: 0)
|
result = BTreeIndex[K, V](root: newBTreeNode[K, V](), order: order, size: 0)
|
||||||
|
initLock(result.lock)
|
||||||
|
|
||||||
proc search[K, V](node: BTreeNode[K, V], key: K): seq[V] =
|
proc search[K, V](node: BTreeNode[K, V], key: K): seq[V] =
|
||||||
var i = 0
|
var i = 0
|
||||||
@@ -84,6 +87,8 @@ proc insertNonFull[K, V](node: BTreeNode[K, V], key: K, value: V, order: int) =
|
|||||||
insertNonFull(node.children[i], key, value, order)
|
insertNonFull(node.children[i], key, value, order)
|
||||||
|
|
||||||
proc insert*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
|
proc insert*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
|
||||||
|
acquire(btree.lock)
|
||||||
|
try:
|
||||||
if btree.root.keys.len == btree.order - 1:
|
if btree.root.keys.len == btree.order - 1:
|
||||||
var newRoot = newBTreeNode[K, V](isLeaf = false)
|
var newRoot = newBTreeNode[K, V](isLeaf = false)
|
||||||
newRoot.children.add(btree.root)
|
newRoot.children.add(btree.root)
|
||||||
@@ -93,14 +98,26 @@ proc insert*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
|
|||||||
else:
|
else:
|
||||||
insertNonFull(btree.root, key, value, btree.order)
|
insertNonFull(btree.root, key, value, btree.order)
|
||||||
inc btree.size
|
inc btree.size
|
||||||
|
finally:
|
||||||
|
release(btree.lock)
|
||||||
|
|
||||||
proc get*[K, V](btree: BTreeIndex[K, V], key: K): seq[V] =
|
proc get*[K, V](btree: BTreeIndex[K, V], key: K): seq[V] =
|
||||||
search(btree.root, key)
|
acquire(btree.lock)
|
||||||
|
try:
|
||||||
|
result = search(btree.root, key)
|
||||||
|
finally:
|
||||||
|
release(btree.lock)
|
||||||
|
|
||||||
proc contains*[K, V](btree: BTreeIndex[K, V], key: K): bool =
|
proc contains*[K, V](btree: BTreeIndex[K, V], key: K): bool =
|
||||||
return btree.get(key).len > 0
|
acquire(btree.lock)
|
||||||
|
try:
|
||||||
|
result = search(btree.root, key).len > 0
|
||||||
|
finally:
|
||||||
|
release(btree.lock)
|
||||||
|
|
||||||
proc scan*[K, V](btree: BTreeIndex[K, V], startKey, endKey: K): seq[(K, seq[V])] =
|
proc scan*[K, V](btree: BTreeIndex[K, V], startKey, endKey: K): seq[(K, seq[V])] =
|
||||||
|
acquire(btree.lock)
|
||||||
|
try:
|
||||||
result = @[]
|
result = @[]
|
||||||
var node = btree.root
|
var node = btree.root
|
||||||
while not node.isLeaf:
|
while not node.isLeaf:
|
||||||
@@ -117,10 +134,19 @@ proc scan*[K, V](btree: BTreeIndex[K, V], startKey, endKey: K): seq[(K, seq[V])]
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
node = node.next
|
node = node.next
|
||||||
|
finally:
|
||||||
|
release(btree.lock)
|
||||||
|
|
||||||
proc len*[K, V](btree: BTreeIndex[K, V]): int = btree.size
|
proc len*[K, V](btree: BTreeIndex[K, V]): int =
|
||||||
|
acquire(btree.lock)
|
||||||
|
try:
|
||||||
|
result = btree.size
|
||||||
|
finally:
|
||||||
|
release(btree.lock)
|
||||||
|
|
||||||
proc remove*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
|
proc remove*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
|
||||||
|
acquire(btree.lock)
|
||||||
|
try:
|
||||||
proc removeRec(node: BTreeNode[K, V]): bool =
|
proc removeRec(node: BTreeNode[K, V]): bool =
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < node.keys.len and key > node.keys[i]:
|
while i < node.keys.len and key > node.keys[i]:
|
||||||
@@ -147,3 +173,5 @@ proc remove*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
|
|||||||
|
|
||||||
if removeRec(btree.root):
|
if removeRec(btree.root):
|
||||||
dec btree.size
|
dec btree.size
|
||||||
|
finally:
|
||||||
|
release(btree.lock)
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
## Wire Protocol Fuzz Tests
|
||||||
|
import std/unittest
|
||||||
|
import std/random
|
||||||
|
import std/strutils
|
||||||
|
|
||||||
|
import barabadb/protocol/wire
|
||||||
|
|
||||||
|
suite "Wire Protocol Fuzz":
|
||||||
|
test "deserializeValue survives random bytes":
|
||||||
|
var rng = initRand(12345)
|
||||||
|
for i in 0..<500:
|
||||||
|
var buf = newSeq[byte](rng.rand(1..256))
|
||||||
|
for b in buf.mitems:
|
||||||
|
b = byte(rng.rand(0..255))
|
||||||
|
var pos = 0
|
||||||
|
try:
|
||||||
|
discard deserializeValue(buf, pos)
|
||||||
|
except:
|
||||||
|
discard # Exceptions are expected for garbage input
|
||||||
|
|
||||||
|
test "deserializeValue survives truncated buffers":
|
||||||
|
var rng = initRand(12346)
|
||||||
|
for i in 0..<200:
|
||||||
|
# Start with a valid-ish prefix then truncate
|
||||||
|
var buf: seq[byte] = @[byte(rng.rand(0..12))] # random FieldKind
|
||||||
|
let extra = rng.rand(0..64)
|
||||||
|
for j in 0..<extra:
|
||||||
|
buf.add(byte(rng.rand(0..255)))
|
||||||
|
var pos = 0
|
||||||
|
try:
|
||||||
|
discard deserializeValue(buf, pos)
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
test "MsgKind cast with random uint32 values":
|
||||||
|
var rng = initRand(12347)
|
||||||
|
for i in 0..<1000:
|
||||||
|
let raw = uint32(rng.rand(int64(high(uint32))))
|
||||||
|
let kind = cast[MsgKind](raw)
|
||||||
|
# Should not crash; cast is unsafe but does not range-check
|
||||||
|
check true
|
||||||
|
|
||||||
|
test "parseHeader-like logic with random 12-byte chunks":
|
||||||
|
var rng = initRand(12348)
|
||||||
|
for i in 0..<500:
|
||||||
|
var data = ""
|
||||||
|
for j in 0..<12:
|
||||||
|
data.add(char(rng.rand(0..255)))
|
||||||
|
let rawKind = uint32(ord(data[0])) shl 24 or uint32(ord(data[1])) shl 16 or
|
||||||
|
uint32(ord(data[2])) shl 8 or uint32(ord(data[3]))
|
||||||
|
let kind = cast[MsgKind](rawKind)
|
||||||
|
let length = uint32(ord(data[4])) shl 24 or uint32(ord(data[5])) shl 16 or
|
||||||
|
uint32(ord(data[6])) shl 8 or uint32(ord(data[7]))
|
||||||
|
let requestId = uint32(ord(data[8])) shl 24 or uint32(ord(data[9])) shl 16 or
|
||||||
|
uint32(ord(data[10])) shl 8 or uint32(ord(data[11]))
|
||||||
|
let header = MessageHeader(kind: kind, length: length, requestId: requestId)
|
||||||
|
check header.length == length
|
||||||
|
|
||||||
|
test "makeQueryMessage roundtrip with random queries":
|
||||||
|
var rng = initRand(12349)
|
||||||
|
for i in 0..<100:
|
||||||
|
var query = ""
|
||||||
|
let len = rng.rand(0..200)
|
||||||
|
for j in 0..<len:
|
||||||
|
query.add(char(rng.rand(32..126)))
|
||||||
|
let msg = makeQueryMessage(uint32(i), query)
|
||||||
|
check msg.len >= 12
|
||||||
|
|
||||||
|
test "serializeMessage roundtrip with random payloads":
|
||||||
|
var rng = initRand(12350)
|
||||||
|
for i in 0..<100:
|
||||||
|
let reqId = uint32(rng.rand(int64(high(uint32))))
|
||||||
|
var msg = WireMessage(
|
||||||
|
header: MessageHeader(kind: mkQuery, length: uint32(rng.rand(0..1000)), requestId: reqId),
|
||||||
|
payload: newSeq[byte](rng.rand(0..200))
|
||||||
|
)
|
||||||
|
for j in 0..<msg.payload.len:
|
||||||
|
msg.payload[j] = byte(rng.rand(0..255))
|
||||||
|
let s = serializeMessage(msg)
|
||||||
|
check s.len >= 12
|
||||||
|
|
||||||
|
test "makeErrorMessage with large IDs":
|
||||||
|
var rng = initRand(12351)
|
||||||
|
for i in 0..<100:
|
||||||
|
let reqId = uint32(rng.rand(int64(high(uint32))))
|
||||||
|
let err = makeErrorMessage(reqId, uint32(rng.rand(0..255)), "fuzz")
|
||||||
|
check err.len > 0
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
## NimForum Adapter Smoke Test
|
||||||
|
import std/unittest
|
||||||
|
import std/osproc
|
||||||
|
import std/os
|
||||||
|
import std/strutils
|
||||||
|
import std/strtabs
|
||||||
|
import std/times
|
||||||
|
|
||||||
|
import ../adaptors/nim/baradb_sqlite as sqlite
|
||||||
|
|
||||||
|
suite "NimForum Adapter Smoke Test":
|
||||||
|
var serverProcess: Process
|
||||||
|
var port: int
|
||||||
|
var dataDir: string
|
||||||
|
|
||||||
|
setup:
|
||||||
|
port = 35000 + (getTime().toUnix.int mod 10000)
|
||||||
|
dataDir = getTempDir() / "baradb_nimforum_" & $port
|
||||||
|
createDir(dataDir)
|
||||||
|
var env = newStringTable()
|
||||||
|
for key, val in envPairs():
|
||||||
|
env[key] = val
|
||||||
|
env["BARADB_PORT"] = $port
|
||||||
|
env["BARADB_DATA_DIR"] = dataDir
|
||||||
|
env["BARADB_LOG_LEVEL"] = "error"
|
||||||
|
serverProcess = startProcess("./build/baradadb", env=env, options={poStdErrToStdOut, poDaemon})
|
||||||
|
sleep(800)
|
||||||
|
|
||||||
|
teardown:
|
||||||
|
if serverProcess != nil:
|
||||||
|
serverProcess.terminate()
|
||||||
|
discard serverProcess.waitForExit()
|
||||||
|
removeDir(dataDir)
|
||||||
|
|
||||||
|
test "Adapter basic CRUD over TCP":
|
||||||
|
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||||
|
|
||||||
|
db.exec(sql"CREATE TABLE nf_test (id INT PRIMARY KEY, name STRING)")
|
||||||
|
db.exec(sql"INSERT INTO nf_test (id, name) VALUES (1, 'hello')")
|
||||||
|
db.exec(sql"INSERT INTO nf_test (id, name) VALUES (2, 'world')")
|
||||||
|
|
||||||
|
let rows = db.getAllRows(sql"SELECT * FROM nf_test")
|
||||||
|
check rows.len == 2
|
||||||
|
|
||||||
|
let row = db.getRow(sql"SELECT * FROM nf_test WHERE id = 1")
|
||||||
|
check row.len == 2
|
||||||
|
check row[0] == "1"
|
||||||
|
check row[1] == "hello"
|
||||||
|
|
||||||
|
let val = db.getValue(sql"SELECT name FROM nf_test WHERE id = 2")
|
||||||
|
check val == "world"
|
||||||
|
|
||||||
|
let cnt = db.getValue(sql"SELECT count(*) FROM nf_test")
|
||||||
|
check cnt == "2"
|
||||||
|
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
test "Adapter parameterized queries":
|
||||||
|
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||||
|
|
||||||
|
db.exec(sql"CREATE TABLE nf_params (id INT PRIMARY KEY, val STRING)")
|
||||||
|
db.exec(sql"INSERT INTO nf_params (id, val) VALUES (?, ?)", 10, "ten")
|
||||||
|
db.exec(sql"INSERT INTO nf_params (id, val) VALUES (?, ?)", 20, "twenty")
|
||||||
|
|
||||||
|
let row = db.getRow(sql"SELECT val FROM nf_params WHERE id = ?", 10)
|
||||||
|
check row.len >= 1
|
||||||
|
check row[row.len - 1] == "ten"
|
||||||
|
|
||||||
|
db.close()
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
## Property-Based Tests — evalExprValue invariants
|
||||||
|
import std/unittest
|
||||||
|
import std/tables
|
||||||
|
import std/random
|
||||||
|
import std/os
|
||||||
|
import std/monotimes
|
||||||
|
import std/math
|
||||||
|
|
||||||
|
import barabadb/core/types
|
||||||
|
import barabadb/storage/lsm
|
||||||
|
import barabadb/query/ir as qir
|
||||||
|
import barabadb/query/executor as qexec
|
||||||
|
|
||||||
|
suite "Property-Based — evalExprValue Invariants":
|
||||||
|
setup:
|
||||||
|
var testDir = getTempDir() / "baradb_prop_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks
|
||||||
|
createDir(testDir)
|
||||||
|
var db = newLSMTree(testDir)
|
||||||
|
var ctx = qexec.newExecutionContext(db)
|
||||||
|
|
||||||
|
teardown:
|
||||||
|
removeDir(testDir)
|
||||||
|
|
||||||
|
proc randIntLit(rng: var Rand, minVal: int = -1000, maxVal: int = 1000): IRExpr =
|
||||||
|
result = IRExpr(kind: irekLiteral, valueKind: vkInt64)
|
||||||
|
result.literal = IRLiteral(kind: vkInt64, int64Val: int64(rng.rand(minVal..maxVal)))
|
||||||
|
|
||||||
|
proc randFloatLit(rng: var Rand, minVal: float = -1000.0, maxVal: float = 1000.0): IRExpr =
|
||||||
|
result = IRExpr(kind: irekLiteral, valueKind: vkFloat64)
|
||||||
|
result.literal = IRLiteral(kind: vkFloat64, float64Val: minVal + rng.rand(maxVal - minVal))
|
||||||
|
|
||||||
|
proc randBinaryExpr(rng: var Rand, left, right: IRExpr, op: IROperator): IRExpr =
|
||||||
|
result = IRExpr(kind: irekBinary)
|
||||||
|
result.binOp = op
|
||||||
|
result.binLeft = left
|
||||||
|
result.binRight = right
|
||||||
|
if left.valueKind == vkInt64 and right.valueKind == vkInt64 and op != irDiv:
|
||||||
|
result.valueKind = vkInt64
|
||||||
|
else:
|
||||||
|
result.valueKind = vkFloat64
|
||||||
|
|
||||||
|
proc randUnaryExpr(rng: var Rand, expr: IRExpr, op: IROperator): IRExpr =
|
||||||
|
result = IRExpr(kind: irekUnary)
|
||||||
|
result.unOp = op
|
||||||
|
result.unExpr = expr
|
||||||
|
result.valueKind = expr.valueKind
|
||||||
|
|
||||||
|
test "Literal eval returns correct ValueKind (INT)":
|
||||||
|
var rng = initRand(42)
|
||||||
|
for i in 0..<100:
|
||||||
|
let lit = randIntLit(rng)
|
||||||
|
let v = evalExprValue(lit, initTable[string, string](), nil)
|
||||||
|
check v.kind == vkInt64
|
||||||
|
|
||||||
|
test "Literal eval returns correct ValueKind (FLOAT)":
|
||||||
|
var rng = initRand(43)
|
||||||
|
for i in 0..<100:
|
||||||
|
let lit = randFloatLit(rng)
|
||||||
|
let v = evalExprValue(lit, initTable[string, string](), nil)
|
||||||
|
check v.kind == vkFloat64
|
||||||
|
|
||||||
|
test "INT addition is commutative":
|
||||||
|
var rng = initRand(44)
|
||||||
|
for i in 0..<100:
|
||||||
|
let a = randIntLit(rng)
|
||||||
|
let b = randIntLit(rng)
|
||||||
|
let sum1 = evalExprValue(randBinaryExpr(rng, a, b, irAdd), initTable[string, string](), nil)
|
||||||
|
let sum2 = evalExprValue(randBinaryExpr(rng, b, a, irAdd), initTable[string, string](), nil)
|
||||||
|
if sum1.kind == vkInt64 and sum2.kind == vkInt64:
|
||||||
|
check sum1.int64Val == sum2.int64Val
|
||||||
|
|
||||||
|
test "FLOAT addition is commutative":
|
||||||
|
var rng = initRand(45)
|
||||||
|
for i in 0..<100:
|
||||||
|
let a = randFloatLit(rng)
|
||||||
|
let b = randFloatLit(rng)
|
||||||
|
let sum1 = evalExprValue(randBinaryExpr(rng, a, b, irAdd), initTable[string, string](), nil)
|
||||||
|
let sum2 = evalExprValue(randBinaryExpr(rng, b, a, irAdd), initTable[string, string](), nil)
|
||||||
|
if sum1.kind == vkFloat64 and sum2.kind == vkFloat64:
|
||||||
|
check abs(sum1.float64Val - sum2.float64Val) < 1e-9
|
||||||
|
|
||||||
|
test "INT multiplication by 1 is identity":
|
||||||
|
var rng = initRand(46)
|
||||||
|
for i in 0..<100:
|
||||||
|
let a = randIntLit(rng)
|
||||||
|
let one = IRExpr(kind: irekLiteral, valueKind: vkInt64)
|
||||||
|
one.literal = IRLiteral(kind: vkInt64, int64Val: 1)
|
||||||
|
let prod = evalExprValue(randBinaryExpr(rng, a, one, irMul), initTable[string, string](), nil)
|
||||||
|
if prod.kind == vkInt64:
|
||||||
|
check prod.int64Val == a.literal.int64Val
|
||||||
|
|
||||||
|
test "Double negation of INT is identity":
|
||||||
|
var rng = initRand(47)
|
||||||
|
for i in 0..<100:
|
||||||
|
let a = randIntLit(rng)
|
||||||
|
let neg = randUnaryExpr(rng, a, irNeg)
|
||||||
|
let negNeg = randUnaryExpr(rng, neg, irNeg)
|
||||||
|
let v = evalExprValue(negNeg, initTable[string, string](), nil)
|
||||||
|
if v.kind == vkInt64:
|
||||||
|
check v.int64Val == a.literal.int64Val
|
||||||
|
|
||||||
|
test "Double negation of FLOAT is identity":
|
||||||
|
var rng = initRand(48)
|
||||||
|
for i in 0..<100:
|
||||||
|
let a = randFloatLit(rng)
|
||||||
|
let neg = randUnaryExpr(rng, a, irNeg)
|
||||||
|
let negNeg = randUnaryExpr(rng, neg, irNeg)
|
||||||
|
let v = evalExprValue(negNeg, initTable[string, string](), nil)
|
||||||
|
if v.kind == vkFloat64:
|
||||||
|
check abs(v.float64Val - a.literal.float64Val) < 1e-9
|
||||||
|
|
||||||
|
test "NULL literal propagates through arithmetic":
|
||||||
|
let nullLit = IRExpr(kind: irekLiteral, valueKind: vkNull)
|
||||||
|
nullLit.literal = IRLiteral(kind: vkNull)
|
||||||
|
let intLit = IRExpr(kind: irekLiteral, valueKind: vkInt64)
|
||||||
|
intLit.literal = IRLiteral(kind: vkInt64, int64Val: 5)
|
||||||
|
for op in [irAdd, irSub, irMul, irDiv]:
|
||||||
|
let expr = IRExpr(kind: irekBinary)
|
||||||
|
expr.binOp = op
|
||||||
|
expr.binLeft = nullLit
|
||||||
|
expr.binRight = intLit
|
||||||
|
let v = evalExprValue(expr, initTable[string, string](), nil)
|
||||||
|
check v.kind == vkNull
|
||||||
@@ -6,6 +6,7 @@ import std/os
|
|||||||
import std/asyncdispatch
|
import std/asyncdispatch
|
||||||
import std/monotimes
|
import std/monotimes
|
||||||
import std/base64
|
import std/base64
|
||||||
|
import std/random
|
||||||
|
|
||||||
import barabadb/core/types
|
import barabadb/core/types
|
||||||
import barabadb/core/mvcc
|
import barabadb/core/mvcc
|
||||||
@@ -3728,3 +3729,5 @@ suite "Join Performance — Hash Join & Index Nested Loop":
|
|||||||
check r.rows[0]["name"] == "Alice"
|
check r.rows[0]["name"] == "Alice"
|
||||||
check r.rows[3]["name"] == "Carol"
|
check r.rows[3]["name"] == "Carol"
|
||||||
check r.rows[3]["total"] == "\\N"
|
check r.rows[3]["total"] == "\\N"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user