Compare commits
2 Commits
a28c845476
...
47c4393a7e
| Author | SHA1 | Date | |
|---|---|---|---|
| 47c4393a7e | |||
| 967c0855a5 |
@@ -109,7 +109,7 @@ proc disconnect*(client: SyncClient) =
|
|||||||
try:
|
try:
|
||||||
let msg = makeQueryMessage(0, "DISCONNECT")
|
let msg = makeQueryMessage(0, "DISCONNECT")
|
||||||
netmod.send(client.socket, cast[string](msg))
|
netmod.send(client.socket, cast[string](msg))
|
||||||
except: discard
|
except IOError, OSError: discard
|
||||||
netmod.close(client.socket)
|
netmod.close(client.socket)
|
||||||
client.connected = false
|
client.connected = false
|
||||||
deinitLock(client.lock)
|
deinitLock(client.lock)
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ proc logRestore*(archive: string, dataDir: string, success: bool, dryRun: bool =
|
|||||||
let f = open(logPath, fmAppend)
|
let f = open(logPath, fmAppend)
|
||||||
f.write(entry)
|
f.write(entry)
|
||||||
f.close()
|
f.close()
|
||||||
except:
|
except IOError:
|
||||||
discard # Silently fail if log cannot be written
|
discard # Silently fail if log cannot be written
|
||||||
|
|
||||||
proc readHistory*(): seq[string] =
|
proc readHistory*(): seq[string] =
|
||||||
@@ -191,7 +191,7 @@ proc readHistory*(): seq[string] =
|
|||||||
for line in splitLines(content):
|
for line in splitLines(content):
|
||||||
if line.len > 0:
|
if line.len > 0:
|
||||||
result.add(line)
|
result.add(line)
|
||||||
except:
|
except IOError:
|
||||||
discard
|
discard
|
||||||
|
|
||||||
proc backupDataDir*(dataDir: string, output: string, excludes: seq[string] = @[], compression: int = DEFAULT_COMPRESSION, verbose: bool = false): bool =
|
proc backupDataDir*(dataDir: string, output: string, excludes: seq[string] = @[], compression: int = DEFAULT_COMPRESSION, verbose: bool = false): bool =
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
## Deadlock Detection — wait-for graph
|
## Deadlock Detection — wait-for graph
|
||||||
import std/tables
|
import std/tables
|
||||||
import std/sets
|
import std/sets
|
||||||
import std/algorithm
|
import std/locks
|
||||||
|
|
||||||
type
|
type
|
||||||
WaitEdge* = object
|
WaitEdge* = object
|
||||||
@@ -12,15 +12,18 @@ type
|
|||||||
edges: seq[WaitEdge]
|
edges: seq[WaitEdge]
|
||||||
adjacency: Table[uint64, seq[uint64]] # waiter -> holders
|
adjacency: Table[uint64, seq[uint64]] # waiter -> holders
|
||||||
txnIds: HashSet[uint64]
|
txnIds: HashSet[uint64]
|
||||||
|
lock: Lock
|
||||||
|
|
||||||
proc newDeadlockDetector*(): DeadlockDetector =
|
proc newDeadlockDetector*(): DeadlockDetector =
|
||||||
DeadlockDetector(
|
new(result)
|
||||||
edges: @[],
|
initLock(result.lock)
|
||||||
adjacency: initTable[uint64, seq[uint64]](),
|
result.edges = @[]
|
||||||
txnIds: initHashSet[uint64](),
|
result.adjacency = initTable[uint64, seq[uint64]]()
|
||||||
)
|
result.txnIds = initHashSet[uint64]()
|
||||||
|
|
||||||
proc addWait*(dd: DeadlockDetector, waiter, holder: uint64) =
|
proc addWait*(dd: DeadlockDetector, waiter, holder: uint64) =
|
||||||
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
dd.edges.add(WaitEdge(waiter: waiter, holder: holder))
|
dd.edges.add(WaitEdge(waiter: waiter, holder: holder))
|
||||||
dd.txnIds.incl(waiter)
|
dd.txnIds.incl(waiter)
|
||||||
dd.txnIds.incl(holder)
|
dd.txnIds.incl(holder)
|
||||||
@@ -29,6 +32,8 @@ proc addWait*(dd: DeadlockDetector, waiter, holder: uint64) =
|
|||||||
dd.adjacency[waiter].add(holder)
|
dd.adjacency[waiter].add(holder)
|
||||||
|
|
||||||
proc removeWait*(dd: DeadlockDetector, waiter, holder: uint64) =
|
proc removeWait*(dd: DeadlockDetector, waiter, holder: uint64) =
|
||||||
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
var newEdges: seq[WaitEdge] = @[]
|
var newEdges: seq[WaitEdge] = @[]
|
||||||
for edge in dd.edges:
|
for edge in dd.edges:
|
||||||
if edge.waiter != waiter or edge.holder != holder:
|
if edge.waiter != waiter or edge.holder != holder:
|
||||||
@@ -43,6 +48,8 @@ proc removeWait*(dd: DeadlockDetector, waiter, holder: uint64) =
|
|||||||
dd.adjacency[waiter] = newAdj
|
dd.adjacency[waiter] = newAdj
|
||||||
|
|
||||||
proc removeTxn*(dd: DeadlockDetector, txnId: uint64) =
|
proc removeTxn*(dd: DeadlockDetector, txnId: uint64) =
|
||||||
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
dd.txnIds.excl(txnId)
|
dd.txnIds.excl(txnId)
|
||||||
dd.adjacency.del(txnId)
|
dd.adjacency.del(txnId)
|
||||||
var newEdges: seq[WaitEdge] = @[]
|
var newEdges: seq[WaitEdge] = @[]
|
||||||
@@ -57,35 +64,28 @@ proc removeTxn*(dd: DeadlockDetector, txnId: uint64) =
|
|||||||
newH.add(h)
|
newH.add(h)
|
||||||
holders = newH
|
holders = newH
|
||||||
|
|
||||||
proc detectCycle*(dd: DeadlockDetector): seq[uint64] =
|
proc detectCycleUnsafe(dd: DeadlockDetector): seq[uint64] =
|
||||||
var visited = initHashSet[uint64]()
|
var visited = initHashSet[uint64]()
|
||||||
var inStack = initHashSet[uint64]()
|
var inStack = initHashSet[uint64]()
|
||||||
var parent = initTable[uint64, uint64]()
|
|
||||||
|
|
||||||
proc dfs(node: uint64): seq[uint64] =
|
proc dfs(node: uint64, path: seq[uint64]): seq[uint64] =
|
||||||
visited.incl(node)
|
visited.incl(node)
|
||||||
inStack.incl(node)
|
inStack.incl(node)
|
||||||
|
var newPath = path & @[node]
|
||||||
for neighbor in dd.adjacency.getOrDefault(node, @[]):
|
for neighbor in dd.adjacency.getOrDefault(node, @[]):
|
||||||
if neighbor in inStack:
|
if neighbor in inStack:
|
||||||
# Found cycle — reconstruct
|
# Found cycle — reconstruct from path
|
||||||
var cycle = @[neighbor, node]
|
var cycle: seq[uint64] = @[]
|
||||||
var current = node
|
var found = false
|
||||||
while parent.getOrDefault(current, 0'u64) != neighbor and
|
for n in newPath:
|
||||||
parent.getOrDefault(current, 0'u64) != 0:
|
if n == neighbor:
|
||||||
current = parent[current]
|
found = true
|
||||||
if current == 0: break
|
if found:
|
||||||
cycle.add(current)
|
cycle.add(n)
|
||||||
# Verify we actually closed the cycle back to neighbor
|
|
||||||
if cycle[^1] != neighbor and parent.getOrDefault(cycle[^1], 0'u64) == neighbor:
|
|
||||||
cycle.add(neighbor)
|
cycle.add(neighbor)
|
||||||
elif cycle[^1] != neighbor:
|
|
||||||
# Incomplete cycle — should not happen with valid parent chain
|
|
||||||
return @[]
|
|
||||||
cycle.reverse()
|
|
||||||
return cycle
|
return cycle
|
||||||
if neighbor notin visited:
|
if neighbor notin visited:
|
||||||
parent[neighbor] = node
|
let cycle = dfs(neighbor, newPath)
|
||||||
let cycle = dfs(neighbor)
|
|
||||||
if cycle.len > 0:
|
if cycle.len > 0:
|
||||||
return cycle
|
return cycle
|
||||||
inStack.excl(node)
|
inStack.excl(node)
|
||||||
@@ -93,13 +93,20 @@ proc detectCycle*(dd: DeadlockDetector): seq[uint64] =
|
|||||||
|
|
||||||
for txnId in dd.txnIds:
|
for txnId in dd.txnIds:
|
||||||
if txnId notin visited:
|
if txnId notin visited:
|
||||||
let cycle = dfs(txnId)
|
let cycle = dfs(txnId, @[])
|
||||||
if cycle.len > 0:
|
if cycle.len > 0:
|
||||||
return cycle
|
return cycle
|
||||||
return @[]
|
return @[]
|
||||||
|
|
||||||
|
proc detectCycle*(dd: DeadlockDetector): seq[uint64] =
|
||||||
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
|
detectCycleUnsafe(dd)
|
||||||
|
|
||||||
proc findDeadlockVictim*(dd: DeadlockDetector): uint64 =
|
proc findDeadlockVictim*(dd: DeadlockDetector): uint64 =
|
||||||
let cycle = dd.detectCycle()
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
|
let cycle = detectCycleUnsafe(dd)
|
||||||
if cycle.len == 0:
|
if cycle.len == 0:
|
||||||
return 0
|
return 0
|
||||||
# Choose youngest txn (highest id) as victim
|
# Choose youngest txn (highest id) as victim
|
||||||
@@ -109,12 +116,23 @@ proc findDeadlockVictim*(dd: DeadlockDetector): uint64 =
|
|||||||
result = id
|
result = id
|
||||||
|
|
||||||
proc hasDeadlock*(dd: DeadlockDetector): bool =
|
proc hasDeadlock*(dd: DeadlockDetector): bool =
|
||||||
return dd.detectCycle().len > 0
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
|
return detectCycleUnsafe(dd).len > 0
|
||||||
|
|
||||||
proc clear*(dd: DeadlockDetector) =
|
proc clear*(dd: DeadlockDetector) =
|
||||||
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
dd.edges.setLen(0)
|
dd.edges.setLen(0)
|
||||||
dd.adjacency.clear()
|
dd.adjacency.clear()
|
||||||
dd.txnIds.clear()
|
dd.txnIds.clear()
|
||||||
|
|
||||||
proc edgeCount*(dd: DeadlockDetector): int = dd.edges.len
|
proc edgeCount*(dd: DeadlockDetector): int =
|
||||||
proc txnCount*(dd: DeadlockDetector): int = dd.txnIds.len
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
|
dd.edges.len
|
||||||
|
|
||||||
|
proc txnCount*(dd: DeadlockDetector): int =
|
||||||
|
acquire(dd.lock)
|
||||||
|
defer: release(dd.lock)
|
||||||
|
dd.txnIds.len
|
||||||
|
|||||||
@@ -108,86 +108,119 @@ proc sendDistTxnRpc(host: string, port: int, txnId: uint64, action: string, time
|
|||||||
except CatchableError:
|
except CatchableError:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
type
|
||||||
|
ParticipantInfo = object
|
||||||
|
nodeId: string
|
||||||
|
host: string
|
||||||
|
port: int
|
||||||
|
|
||||||
proc prepare*(txn: DistributedTransaction): bool =
|
proc prepare*(txn: DistributedTransaction): bool =
|
||||||
|
# Phase 1: validate state and collect participants while holding lock
|
||||||
|
var participants: seq[ParticipantInfo] = @[]
|
||||||
|
var wasActive = false
|
||||||
acquire(txn.lock)
|
acquire(txn.lock)
|
||||||
if txn.state != dtsActive:
|
if txn.state == dtsActive:
|
||||||
|
txn.state = dtsPreparing
|
||||||
|
wasActive = true
|
||||||
|
for nodeId, participant in txn.participants:
|
||||||
|
if participant.host.len > 0 and participant.port > 0:
|
||||||
|
participants.add(ParticipantInfo(nodeId: nodeId, host: participant.host, port: participant.port))
|
||||||
release(txn.lock)
|
release(txn.lock)
|
||||||
|
|
||||||
|
if not wasActive:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
txn.state = dtsPreparing
|
# Phase 2: perform network I/O without holding the lock
|
||||||
|
|
||||||
var allOk = true
|
|
||||||
var preparedNodes: seq[string] = @[]
|
var preparedNodes: seq[string] = @[]
|
||||||
for nodeId, participant in txn.participants.mpairs:
|
var allOk = true
|
||||||
if participant.host.len > 0 and participant.port > 0:
|
for p in participants:
|
||||||
participant.prepared = sendDistTxnRpc(participant.host, participant.port, txn.id, "PREPARE")
|
let ok = sendDistTxnRpc(p.host, p.port, txn.id, "PREPARE")
|
||||||
else:
|
if ok:
|
||||||
participant.prepared = true # local participant
|
preparedNodes.add(p.nodeId)
|
||||||
if participant.prepared:
|
|
||||||
preparedNodes.add(nodeId)
|
|
||||||
else:
|
else:
|
||||||
allOk = false
|
allOk = false
|
||||||
|
|
||||||
|
# Phase 3: update state while holding lock
|
||||||
|
acquire(txn.lock)
|
||||||
if allOk:
|
if allOk:
|
||||||
txn.state = dtsPrepared
|
txn.state = dtsPrepared
|
||||||
|
for nodeId, _ in txn.participants.mpairs:
|
||||||
|
txn.participants[nodeId].prepared = true
|
||||||
else:
|
else:
|
||||||
# Rollback already-prepared participants to maintain atomicity
|
# Rollback already-prepared participants
|
||||||
for nodeId in preparedNodes:
|
for nodeId in preparedNodes:
|
||||||
if txn.participants[nodeId].host.len > 0 and txn.participants[nodeId].port > 0:
|
if txn.participants.hasKey(nodeId):
|
||||||
discard sendDistTxnRpc(txn.participants[nodeId].host, txn.participants[nodeId].port, txn.id, "ROLLBACK")
|
discard sendDistTxnRpc(txn.participants[nodeId].host, txn.participants[nodeId].port, txn.id, "ROLLBACK")
|
||||||
txn.participants[nodeId].prepared = false
|
txn.participants[nodeId].prepared = false
|
||||||
txn.participants[nodeId].aborted = true
|
txn.participants[nodeId].aborted = true
|
||||||
txn.state = dtsAborted
|
txn.state = dtsAborted
|
||||||
|
|
||||||
release(txn.lock)
|
release(txn.lock)
|
||||||
return allOk
|
return allOk
|
||||||
|
|
||||||
proc commit*(txn: DistributedTransaction): bool =
|
proc commit*(txn: DistributedTransaction): bool =
|
||||||
|
# Phase 1: validate state and collect participants while holding lock
|
||||||
|
var participants: seq[ParticipantInfo] = @[]
|
||||||
|
var wasPrepared = false
|
||||||
acquire(txn.lock)
|
acquire(txn.lock)
|
||||||
if txn.state != dtsPrepared:
|
if txn.state == dtsPrepared:
|
||||||
|
txn.state = dtsCommitting
|
||||||
|
wasPrepared = true
|
||||||
|
for nodeId, participant in txn.participants:
|
||||||
|
if participant.host.len > 0 and participant.port > 0:
|
||||||
|
participants.add(ParticipantInfo(nodeId: nodeId, host: participant.host, port: participant.port))
|
||||||
release(txn.lock)
|
release(txn.lock)
|
||||||
|
|
||||||
|
if not wasPrepared:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
txn.state = dtsCommitting
|
# Phase 2: perform network I/O without holding the lock
|
||||||
|
|
||||||
var allOk = true
|
|
||||||
var committedNodes: seq[string] = @[]
|
var committedNodes: seq[string] = @[]
|
||||||
for nodeId, participant in txn.participants.mpairs:
|
var allOk = true
|
||||||
if participant.host.len > 0 and participant.port > 0:
|
for p in participants:
|
||||||
participant.committed = sendDistTxnRpc(participant.host, participant.port, txn.id, "COMMIT")
|
let ok = sendDistTxnRpc(p.host, p.port, txn.id, "COMMIT")
|
||||||
else:
|
if ok:
|
||||||
participant.committed = true # local participant
|
committedNodes.add(p.nodeId)
|
||||||
if participant.committed:
|
|
||||||
committedNodes.add(nodeId)
|
|
||||||
else:
|
else:
|
||||||
allOk = false
|
allOk = false
|
||||||
|
|
||||||
|
# Phase 3: update state while holding lock
|
||||||
|
acquire(txn.lock)
|
||||||
if allOk:
|
if allOk:
|
||||||
txn.state = dtsCommitted
|
txn.state = dtsCommitted
|
||||||
else:
|
for nodeId, _ in txn.participants.mpairs:
|
||||||
if committedNodes.len > 0:
|
txn.participants[nodeId].committed = true
|
||||||
# Some participants already committed — cannot rollback committed nodes
|
elif committedNodes.len > 0:
|
||||||
# without violating atomicity. Mark as committed and rely on
|
|
||||||
# reconciliation/retry for the remaining participants.
|
|
||||||
txn.state = dtsCommitted
|
txn.state = dtsCommitted
|
||||||
|
for nodeId in committedNodes:
|
||||||
|
if txn.participants.hasKey(nodeId):
|
||||||
|
txn.participants[nodeId].committed = true
|
||||||
else:
|
else:
|
||||||
# No participant committed yet — safe to abort
|
|
||||||
txn.state = dtsAborted
|
txn.state = dtsAborted
|
||||||
release(txn.lock)
|
release(txn.lock)
|
||||||
return allOk or committedNodes.len > 0
|
return allOk or committedNodes.len > 0
|
||||||
|
|
||||||
proc rollback*(txn: DistributedTransaction): bool =
|
proc rollback*(txn: DistributedTransaction): bool =
|
||||||
|
# Phase 1: validate state and collect participants while holding lock
|
||||||
|
var participants: seq[ParticipantInfo] = @[]
|
||||||
|
var canRollback = false
|
||||||
acquire(txn.lock)
|
acquire(txn.lock)
|
||||||
if txn.state notin {dtsActive, dtsPreparing, dtsPrepared}:
|
if txn.state in {dtsActive, dtsPreparing, dtsPrepared}:
|
||||||
|
txn.state = dtsAborting
|
||||||
|
canRollback = true
|
||||||
|
for nodeId, participant in txn.participants:
|
||||||
|
if participant.host.len > 0 and participant.port > 0:
|
||||||
|
participants.add(ParticipantInfo(nodeId: nodeId, host: participant.host, port: participant.port))
|
||||||
release(txn.lock)
|
release(txn.lock)
|
||||||
|
|
||||||
|
if not canRollback:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
txn.state = dtsAborting
|
# Phase 2: perform network I/O without holding the lock
|
||||||
for nodeId, participant in txn.participants.mpairs:
|
for p in participants:
|
||||||
if participant.host.len > 0 and participant.port > 0:
|
discard sendDistTxnRpc(p.host, p.port, txn.id, "ROLLBACK")
|
||||||
participant.aborted = sendDistTxnRpc(participant.host, participant.port, txn.id, "ROLLBACK")
|
|
||||||
else:
|
# Phase 3: update state while holding lock
|
||||||
participant.aborted = true
|
acquire(txn.lock)
|
||||||
txn.state = dtsAborted
|
txn.state = dtsAborted
|
||||||
release(txn.lock)
|
release(txn.lock)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ proc serialize*(msg: GossipMessage): seq[byte] =
|
|||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
proc deserializeGossipMessage*(data: seq[byte]): GossipMessage =
|
proc deserializeGossipMessage*(data: seq[byte]): GossipMessage =
|
||||||
|
const MaxGossipIdLen = 256
|
||||||
|
const MaxGossipNodeCount = 4096
|
||||||
|
const MaxGossipHostLen = 256
|
||||||
let s = newStringStream(cast[string](data))
|
let s = newStringStream(cast[string](data))
|
||||||
let magic = s.readStr(4)
|
let magic = s.readStr(4)
|
||||||
if magic != GossipMagic:
|
if magic != GossipMagic:
|
||||||
@@ -83,16 +86,24 @@ proc deserializeGossipMessage*(data: seq[byte]): GossipMessage =
|
|||||||
if version != GossipProtoVersion:
|
if version != GossipProtoVersion:
|
||||||
raise newException(ValueError, "Unsupported gossip protocol version")
|
raise newException(ValueError, "Unsupported gossip protocol version")
|
||||||
let senderIdLen = int(s.readUint32())
|
let senderIdLen = int(s.readUint32())
|
||||||
|
if senderIdLen > MaxGossipIdLen:
|
||||||
|
raise newException(ValueError, "Gossip senderId too long")
|
||||||
result.senderId = if senderIdLen > 0: s.readStr(senderIdLen) else: ""
|
result.senderId = if senderIdLen > 0: s.readStr(senderIdLen) else: ""
|
||||||
result.senderIncarnation = s.readUint64()
|
result.senderIncarnation = s.readUint64()
|
||||||
let nodeCount = int(s.readUint32())
|
let nodeCount = int(s.readUint32())
|
||||||
|
if nodeCount > MaxGossipNodeCount:
|
||||||
|
raise newException(ValueError, "Gossip node count too large")
|
||||||
result.nodes = newSeq[(string, NodeState, uint64, string, int)](nodeCount)
|
result.nodes = newSeq[(string, NodeState, uint64, string, int)](nodeCount)
|
||||||
for i in 0 ..< nodeCount:
|
for i in 0 ..< nodeCount:
|
||||||
let idLen = int(s.readUint32())
|
let idLen = int(s.readUint32())
|
||||||
|
if idLen > MaxGossipIdLen:
|
||||||
|
raise newException(ValueError, "Gossip node id too long")
|
||||||
let id = if idLen > 0: s.readStr(idLen) else: ""
|
let id = if idLen > 0: s.readStr(idLen) else: ""
|
||||||
let state = NodeState(s.readUint32())
|
let state = NodeState(s.readUint32())
|
||||||
let incarnation = s.readUint64()
|
let incarnation = s.readUint64()
|
||||||
let hostLen = int(s.readUint32())
|
let hostLen = int(s.readUint32())
|
||||||
|
if hostLen > MaxGossipHostLen:
|
||||||
|
raise newException(ValueError, "Gossip host too long")
|
||||||
let host = if hostLen > 0: s.readStr(hostLen) else: ""
|
let host = if hostLen > 0: s.readStr(hostLen) else: ""
|
||||||
let port = int(s.readUint32())
|
let port = int(s.readUint32())
|
||||||
result.nodes[i] = (id, state, incarnation, host, port)
|
result.nodes[i] = (id, state, incarnation, host, port)
|
||||||
|
|||||||
@@ -115,18 +115,25 @@ proc loadState(node: RaftNode) =
|
|||||||
if votedForLen > 0:
|
if votedForLen > 0:
|
||||||
node.votedFor = s.readStr(votedForLen)
|
node.votedFor = s.readStr(votedForLen)
|
||||||
let logLen = int(s.readUint32())
|
let logLen = int(s.readUint32())
|
||||||
|
if logLen > 1_000_000:
|
||||||
|
raise newException(ValueError, "Raft log length too large")
|
||||||
node.log = newSeq[LogEntry](logLen)
|
node.log = newSeq[LogEntry](logLen)
|
||||||
for i in 0..<logLen:
|
for i in 0..<logLen:
|
||||||
let term = s.readUint64()
|
let term = s.readUint64()
|
||||||
let index = s.readUint64()
|
let index = s.readUint64()
|
||||||
let cmdLen = int(s.readUint32())
|
let cmdLen = int(s.readUint32())
|
||||||
|
if cmdLen > 1_000_000:
|
||||||
|
raise newException(ValueError, "Raft command length too large")
|
||||||
let cmd = s.readStr(cmdLen)
|
let cmd = s.readStr(cmdLen)
|
||||||
let dataLen = int(s.readUint32())
|
let dataLen = int(s.readUint32())
|
||||||
|
if dataLen > 10_000_000:
|
||||||
|
raise newException(ValueError, "Raft data length too large")
|
||||||
var data = newSeq[byte](dataLen)
|
var data = newSeq[byte](dataLen)
|
||||||
if dataLen > 0:
|
if dataLen > 0:
|
||||||
discard s.readData(addr data[0], dataLen)
|
if s.readData(addr data[0], dataLen) != dataLen:
|
||||||
|
raise newException(IOError, "Incomplete Raft log data read")
|
||||||
node.log[i] = LogEntry(term: term, index: index, command: cmd, data: data)
|
node.log[i] = LogEntry(term: term, index: index, command: cmd, data: data)
|
||||||
except:
|
except IOError, OSError:
|
||||||
discard
|
discard
|
||||||
s.close()
|
s.close()
|
||||||
|
|
||||||
@@ -431,7 +438,8 @@ proc readString(s: Stream): string =
|
|||||||
let len = int(s.readUint32())
|
let len = int(s.readUint32())
|
||||||
if len > 0:
|
if len > 0:
|
||||||
result = newString(len)
|
result = newString(len)
|
||||||
discard s.readData(result[0].addr, len)
|
if s.readData(result[0].addr, len) != len:
|
||||||
|
raise newException(IOError, "Incomplete string read from stream")
|
||||||
else:
|
else:
|
||||||
result = ""
|
result = ""
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ proc newServer*(config: BaraConfig): Server =
|
|||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
proc readUint32BE(data: string, pos: int): uint32 =
|
proc readUint32BE(data: string, pos: int): uint32 =
|
||||||
|
if pos + 4 > data.len:
|
||||||
|
raise newException(ValueError, "readUint32BE: index out of bounds")
|
||||||
var bytes: array[4, byte]
|
var bytes: array[4, byte]
|
||||||
for i in 0..3:
|
for i in 0..3:
|
||||||
bytes[i] = byte(data[pos + i])
|
bytes[i] = byte(data[pos + i])
|
||||||
@@ -108,13 +110,9 @@ 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)
|
||||||
let kind = cast[MsgKind](rawKind)
|
if rawKind < 0x01 or (rawKind > 0x09 and rawKind < 0x80) or rawKind > 0x89:
|
||||||
case kind
|
|
||||||
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())
|
return (false, MessageHeader())
|
||||||
|
let kind = cast[MsgKind](rawKind)
|
||||||
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))
|
||||||
@@ -143,11 +141,11 @@ proc valueToWire(val: string, colType: string): WireValue =
|
|||||||
if t.startsWith("INT") or t == "SERIAL" or t == "BIGINT" or t == "SMALLINT" or t == "BIGSERIAL" or t == "SMALLSERIAL":
|
if t.startsWith("INT") or t == "SERIAL" or t == "BIGINT" or t == "SMALLINT" or t == "BIGSERIAL" or t == "SMALLSERIAL":
|
||||||
try:
|
try:
|
||||||
return WireValue(kind: fkInt64, int64Val: parseInt(val))
|
return WireValue(kind: fkInt64, int64Val: parseInt(val))
|
||||||
except: discard
|
except ValueError: discard
|
||||||
elif t.startsWith("FLOAT") or t == "REAL" or t == "DOUBLE" or t == "NUMERIC" or t.startsWith("DOUBLE"):
|
elif t.startsWith("FLOAT") or t == "REAL" or t == "DOUBLE" or t == "NUMERIC" or t.startsWith("DOUBLE"):
|
||||||
try:
|
try:
|
||||||
return WireValue(kind: fkFloat64, float64Val: parseFloat(val))
|
return WireValue(kind: fkFloat64, float64Val: parseFloat(val))
|
||||||
except: discard
|
except ValueError: discard
|
||||||
elif t == "BOOLEAN" or t == "BOOL":
|
elif t == "BOOLEAN" or t == "BOOL":
|
||||||
let lv = val.toLower()
|
let lv = val.toLower()
|
||||||
if lv in ["true", "t", "yes", "1"]:
|
if lv in ["true", "t", "yes", "1"]:
|
||||||
@@ -286,7 +284,7 @@ proc slowQueryLog(logPath: string, query: string, durationMs: int, clientId: int
|
|||||||
defer: f.close()
|
defer: f.close()
|
||||||
let line = $getMonoTime().ticks() & " | " & $clientId & " | " & $durationMs & "ms | " & query & "\n"
|
let line = $getMonoTime().ticks() & " | " & $clientId & " | " & $durationMs & "ms | " & query & "\n"
|
||||||
f.write(line)
|
f.write(line)
|
||||||
except: discard
|
except IOError: discard
|
||||||
|
|
||||||
proc verifyToken(secret, tokenStr: string): (bool, string, string) =
|
proc verifyToken(secret, tokenStr: string): (bool, string, string) =
|
||||||
try:
|
try:
|
||||||
@@ -296,7 +294,7 @@ proc verifyToken(secret, tokenStr: string): (bool, string, string) =
|
|||||||
let userId = token.claims["sub"].node.str
|
let userId = token.claims["sub"].node.str
|
||||||
let role = if "role" in token.claims: token.claims["role"].node.str else: "user"
|
let role = if "role" in token.claims: token.claims["role"].node.str else: "user"
|
||||||
return (true, userId, role)
|
return (true, userId, role)
|
||||||
except:
|
except ValueError, KeyError:
|
||||||
return (false, "", "")
|
return (false, "", "")
|
||||||
|
|
||||||
proc recvWithTimeout(client: AsyncSocket, size: int, timeoutMs: int): Future[string] {.async.} =
|
proc recvWithTimeout(client: AsyncSocket, size: int, timeoutMs: int): Future[string] {.async.} =
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -110,10 +110,13 @@ proc constantTimeCompare(a, b: string): bool =
|
|||||||
# JWT token helpers
|
# JWT token helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc jsonEscape(s: string): string =
|
||||||
|
result = s.replace("\\", "\\\\").replace("\"", "\\\"")
|
||||||
|
|
||||||
proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
||||||
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
|
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
|
||||||
var payloadJson = "{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role &
|
var payloadJson = "{\"sub\":\"" & jsonEscape(claims.sub) & "\",\"role\":\"" & jsonEscape(claims.role) &
|
||||||
"\",\"database\":\"" & claims.database & "\""
|
"\",\"database\":\"" & jsonEscape(claims.database) & "\""
|
||||||
if claims.exp > 0:
|
if claims.exp > 0:
|
||||||
payloadJson &= ",\"exp\":" & $claims.exp
|
payloadJson &= ",\"exp\":" & $claims.exp
|
||||||
if claims.iat > 0:
|
if claims.iat > 0:
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ proc hmacSha256*(key, message: string): array[32, byte] =
|
|||||||
var ctx = initSha_256()
|
var ctx = initSha_256()
|
||||||
ctx.update(k.toOpenArray(0, k.len-1))
|
ctx.update(k.toOpenArray(0, k.len-1))
|
||||||
let hash = ctx.digest()
|
let hash = ctx.digest()
|
||||||
k = $hash
|
var kHashed = newString(32)
|
||||||
|
copyMem(addr kHashed[0], unsafeAddr hash[0], 32)
|
||||||
|
k = kHashed
|
||||||
while k.len < 64:
|
while k.len < 64:
|
||||||
k &= "\x00"
|
k &= "\x00"
|
||||||
|
|
||||||
@@ -130,6 +132,8 @@ proc generateNonce*(): string =
|
|||||||
## Generate a cryptographically secure random nonce (base64-encoded).
|
## Generate a cryptographically secure random nonce (base64-encoded).
|
||||||
when defined(linux) or defined(macosx) or defined(bsd):
|
when defined(linux) or defined(macosx) or defined(bsd):
|
||||||
let f = open("/dev/urandom")
|
let f = open("/dev/urandom")
|
||||||
|
if f == nil:
|
||||||
|
raise newException(IOError, "Failed to open /dev/urandom")
|
||||||
defer: f.close()
|
defer: f.close()
|
||||||
var bytes = newString(NonceBytes)
|
var bytes = newString(NonceBytes)
|
||||||
let readLen = f.readBuffer(addr bytes[0], NonceBytes)
|
let readLen = f.readBuffer(addr bytes[0], NonceBytes)
|
||||||
@@ -209,18 +213,18 @@ proc parseClientFinal*(msg: string): (string, string, seq[byte]) =
|
|||||||
## Returns: (channel_binding, nonce, proof_bytes)
|
## Returns: (channel_binding, nonce, proof_bytes)
|
||||||
var cbind = ""
|
var cbind = ""
|
||||||
var nonce = ""
|
var nonce = ""
|
||||||
var proofHex = ""
|
var proofEncoded = ""
|
||||||
for p in msg.split(","):
|
for p in msg.split(","):
|
||||||
if p.startsWith("c="):
|
if p.startsWith("c="):
|
||||||
cbind = p[2..^1]
|
cbind = p[2..^1]
|
||||||
elif p.startsWith("r="):
|
elif p.startsWith("r="):
|
||||||
nonce = p[2..^1]
|
nonce = p[2..^1]
|
||||||
elif p.startsWith("p="):
|
elif p.startsWith("p="):
|
||||||
proofHex = p[2..^1]
|
proofEncoded = p[2..^1]
|
||||||
if nonce.len == 0 or proofHex.len == 0:
|
if nonce.len == 0 or proofEncoded.len == 0:
|
||||||
raise newException(ValueError, "Missing nonce or proof in client-final-message")
|
raise newException(ValueError, "Missing nonce or proof in client-final-message")
|
||||||
# proof is base64-encoded
|
# proof is base64-encoded
|
||||||
var proof = decode(proofHex)
|
var proof = decode(proofEncoded)
|
||||||
var proofBytes = newSeq[byte](proof.len)
|
var proofBytes = newSeq[byte](proof.len)
|
||||||
if proof.len > 0:
|
if proof.len > 0:
|
||||||
copyMem(addr proofBytes[0], addr proof[0], proof.len)
|
copyMem(addr proofBytes[0], addr proof[0], proof.len)
|
||||||
@@ -240,6 +244,8 @@ proc buildServerFinal*(serverSignature: openArray[byte]): string =
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc verifyClientProof*(state: ScramServerState, clientProof: openArray[byte]): bool =
|
proc verifyClientProof*(state: ScramServerState, clientProof: openArray[byte]): bool =
|
||||||
|
if clientProof.len != 32:
|
||||||
|
return false
|
||||||
let clientKey = xorBytes(clientProof, @(hmacSha256(state.storedKey, state.authMessage)))
|
let clientKey = xorBytes(clientProof, @(hmacSha256(state.storedKey, state.authMessage)))
|
||||||
let computedStoredKey = sha256(clientKey)
|
let computedStoredKey = sha256(clientKey)
|
||||||
for i in 0..<32:
|
for i in 0..<32:
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): Wire
|
|||||||
result = WireValue(kind: fkBytes, bytesVal: readBytes(buf, pos))
|
result = WireValue(kind: fkBytes, bytesVal: readBytes(buf, pos))
|
||||||
of fkArray:
|
of fkArray:
|
||||||
let count = int(readUint32(buf, pos))
|
let count = int(readUint32(buf, pos))
|
||||||
if count > MaxWireArrayLen:
|
if count < 0 or count > MaxWireArrayLen:
|
||||||
raise newException(ValueError, "Wire protocol: array exceeds max length")
|
raise newException(ValueError, "Wire protocol: array exceeds max length")
|
||||||
var arr: seq[WireValue] = @[]
|
var arr: seq[WireValue] = @[]
|
||||||
for i in 0..<count:
|
for i in 0..<count:
|
||||||
@@ -253,7 +253,7 @@ proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): Wire
|
|||||||
result = WireValue(kind: fkArray, arrayVal: arr)
|
result = WireValue(kind: fkArray, arrayVal: arr)
|
||||||
of fkObject:
|
of fkObject:
|
||||||
let count = int(readUint32(buf, pos))
|
let count = int(readUint32(buf, pos))
|
||||||
if count > MaxWireObjectLen:
|
if count < 0 or count > MaxWireObjectLen:
|
||||||
raise newException(ValueError, "Wire protocol: object exceeds max length")
|
raise newException(ValueError, "Wire protocol: object exceeds max length")
|
||||||
var obj: seq[(string, WireValue)] = @[]
|
var obj: seq[(string, WireValue)] = @[]
|
||||||
for i in 0..<count:
|
for i in 0..<count:
|
||||||
@@ -263,7 +263,7 @@ proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): Wire
|
|||||||
result = WireValue(kind: fkObject, objVal: obj)
|
result = WireValue(kind: fkObject, objVal: obj)
|
||||||
of fkVector:
|
of fkVector:
|
||||||
let count = int(readUint32(buf, pos))
|
let count = int(readUint32(buf, pos))
|
||||||
if count > MaxWireVectorLen:
|
if count < 0 or count > MaxWireVectorLen:
|
||||||
raise newException(ValueError, "Wire protocol: vector exceeds max length")
|
raise newException(ValueError, "Wire protocol: vector exceeds max length")
|
||||||
if pos + count * 4 > buf.len:
|
if pos + count * 4 > buf.len:
|
||||||
raise newException(ValueError, "Wire protocol: truncated vector data")
|
raise newException(ValueError, "Wire protocol: truncated vector data")
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ proc readString*(buf: ZeroBuf, offset: var int): string =
|
|||||||
let len = int(buf.readInt64(offset))
|
let len = int(buf.readInt64(offset))
|
||||||
offset += 8
|
offset += 8
|
||||||
if len > 0:
|
if len > 0:
|
||||||
|
if offset + len > buf.capacity:
|
||||||
|
raise newException(ValueError, "ZeroCopy readString: buffer overflow")
|
||||||
result = newString(len)
|
result = newString(len)
|
||||||
copyMem(addr result[0], addr buf.data[offset], len)
|
copyMem(addr result[0], addr buf.data[offset], len)
|
||||||
offset += len
|
offset += len
|
||||||
|
|||||||
+100
-51
@@ -185,6 +185,14 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
|||||||
# AST to SQL serializer (for VIEW DDL persistence)
|
# AST to SQL serializer (for VIEW DDL persistence)
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc sqlEscapeIdent*(ident: string): string =
|
||||||
|
## Escape SQL identifiers by doubling double-quotes.
|
||||||
|
result = ident.replace("\"", "\"\"")
|
||||||
|
|
||||||
|
proc sqlEscapeString*(s: string): string =
|
||||||
|
## Escape SQL string literals by doubling single-quotes.
|
||||||
|
result = s.replace("'", "''")
|
||||||
|
|
||||||
proc exprToSql(node: Node): string =
|
proc exprToSql(node: Node): string =
|
||||||
if node == nil:
|
if node == nil:
|
||||||
return ""
|
return ""
|
||||||
@@ -200,7 +208,7 @@ proc exprToSql(node: Node): string =
|
|||||||
of nkNullLit:
|
of nkNullLit:
|
||||||
return "null"
|
return "null"
|
||||||
of nkIdent:
|
of nkIdent:
|
||||||
return node.identName
|
return "\"" & node.identName.replace("\"", "\"\"") & "\""
|
||||||
of nkStar:
|
of nkStar:
|
||||||
return "*"
|
return "*"
|
||||||
of nkBinOp:
|
of nkBinOp:
|
||||||
@@ -297,8 +305,13 @@ proc restoreSchema(ctx: ExecutionContext) =
|
|||||||
if not entry.key.startsWith("_schema:"): continue
|
if not entry.key.startsWith("_schema:"): continue
|
||||||
let ddl = cast[string](entry.value)
|
let ddl = cast[string](entry.value)
|
||||||
if ddl.len == 0: continue
|
if ddl.len == 0: continue
|
||||||
|
var astNode: Node
|
||||||
|
try:
|
||||||
let tokens = qlex.tokenize(ddl)
|
let tokens = qlex.tokenize(ddl)
|
||||||
let astNode = qpar.parse(tokens)
|
astNode = qpar.parse(tokens)
|
||||||
|
except:
|
||||||
|
# Skip corrupted schema entries during startup
|
||||||
|
continue
|
||||||
if astNode.stmts.len > 0:
|
if astNode.stmts.len > 0:
|
||||||
let stmt = astNode.stmts[0]
|
let stmt = astNode.stmts[0]
|
||||||
case stmt.kind
|
case stmt.kind
|
||||||
@@ -992,9 +1005,15 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
if expr.binRight.kind == irekSubquery:
|
if expr.binRight.kind == irekSubquery:
|
||||||
let subRows = executePlan(ctx, expr.binRight.subqueryPlan)
|
let subRows = executePlan(ctx, expr.binRight.subqueryPlan)
|
||||||
for row in subRows:
|
for row in subRows:
|
||||||
|
# Compare against the first non-internal column only (SQL semantics)
|
||||||
|
var firstVal = ""
|
||||||
|
var found = false
|
||||||
for k, v in row:
|
for k, v in row:
|
||||||
if k.startsWith("$"): continue
|
if k.startsWith("$"): continue
|
||||||
if v == left: return "true"
|
firstVal = v
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
if found and firstVal == left: return "true"
|
||||||
return "false"
|
return "false"
|
||||||
try:
|
try:
|
||||||
let lv = parseFloat(left)
|
let lv = parseFloat(left)
|
||||||
@@ -1006,9 +1025,15 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
if expr.binRight.kind == irekSubquery:
|
if expr.binRight.kind == irekSubquery:
|
||||||
let subRows = executePlan(ctx, expr.binRight.subqueryPlan)
|
let subRows = executePlan(ctx, expr.binRight.subqueryPlan)
|
||||||
for row in subRows:
|
for row in subRows:
|
||||||
|
# Compare against the first non-internal column only (SQL semantics)
|
||||||
|
var firstVal = ""
|
||||||
|
var found = false
|
||||||
for k, v in row:
|
for k, v in row:
|
||||||
if k.startsWith("$"): continue
|
if k.startsWith("$"): continue
|
||||||
if v == left: return "false"
|
firstVal = v
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
if found and firstVal == left: return "false"
|
||||||
return "true"
|
return "true"
|
||||||
try:
|
try:
|
||||||
let lv = parseFloat(left)
|
let lv = parseFloat(left)
|
||||||
@@ -1216,10 +1241,13 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
let queryVec = evalExpr(expr.irFuncArgs[4], row, ctx)
|
let queryVec = evalExpr(expr.irFuncArgs[4], row, ctx)
|
||||||
let k = try: parseInt(evalExpr(expr.irFuncArgs[5], row, ctx)) except ValueError: 10
|
let k = try: parseInt(evalExpr(expr.irFuncArgs[5], row, ctx)) except ValueError: 10
|
||||||
let results = doHybridSearch(ctx, table, vecCol, textCol, queryText, queryVec, k)
|
let results = doHybridSearch(ctx, table, vecCol, textCol, queryText, queryVec, k)
|
||||||
var parts: seq[string] = @[]
|
var jsonArr = newJArray()
|
||||||
for (id, score) in results:
|
for (id, score) in results:
|
||||||
parts.add("{\"id\":\"" & $id & "\",\"score\":\"" & $score & "\"}")
|
var obj = newJObject()
|
||||||
return "[" & parts.join(",") & "]"
|
obj["id"] = %id
|
||||||
|
obj["score"] = %score
|
||||||
|
jsonArr.add(obj)
|
||||||
|
return $jsonArr
|
||||||
of "hybrid_search_ids":
|
of "hybrid_search_ids":
|
||||||
if expr.irFuncArgs.len < 6: return ""
|
if expr.irFuncArgs.len < 6: return ""
|
||||||
let table = evalExpr(expr.irFuncArgs[0], row, ctx)
|
let table = evalExpr(expr.irFuncArgs[0], row, ctx)
|
||||||
@@ -1334,10 +1362,31 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
if sql.len == 0:
|
if sql.len == 0:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# Validate by trying EXPLAIN or LIMIT-wrapped query
|
# Validate by parsing only (never execute LLM-generated SQL directly)
|
||||||
|
let sqlLower = sql.toLower().strip()
|
||||||
|
let isSafeQuery = sqlLower.startsWith("select") or sqlLower.startsWith("explain") or
|
||||||
|
sqlLower.startsWith("with")
|
||||||
|
let allowDml = ctx.sessionVars.getOrDefault("nl_to_sql.allow_dml", "false") == "true"
|
||||||
|
if not isSafeQuery and not allowDml:
|
||||||
|
# For non-SELECT: only do syntax validation via tokenize+parse, no execution
|
||||||
|
let tokens = qlex.tokenize(sql)
|
||||||
|
let astNode = qpar.parse(tokens)
|
||||||
|
if astNode.stmts.len > 0:
|
||||||
|
return sql
|
||||||
|
else:
|
||||||
|
let correctionPrompt = "Schema:\n" & schemaInfo & "\nQuestion: " & question & "\n\nPrevious SQL: " & sql & "\n\nError: Syntax error. Generate corrected SQL:"
|
||||||
|
var correctedResponse = llmmod.generate(ctx.llmClient, correctionPrompt, systemPrompt)
|
||||||
|
var correctedSql = llmmod.extractSQL(correctedResponse)
|
||||||
|
if correctedSql.len > 0:
|
||||||
|
return correctedSql
|
||||||
|
return ""
|
||||||
|
else:
|
||||||
|
# For SELECT/EXPLAIN or when ALLOW_DML is set: validate with LIMIT 0 or EXPLAIN
|
||||||
var validateSql = sql
|
var validateSql = sql
|
||||||
if validateSql.toLower().startsWith("select"):
|
if sqlLower.startsWith("select"):
|
||||||
validateSql = "SELECT * FROM (" & sql & ") LIMIT 0"
|
validateSql = "SELECT * FROM (" & sql & ") LIMIT 0"
|
||||||
|
elif sqlLower.startsWith("with"):
|
||||||
|
validateSql = "WITH _cte_ AS (" & sql & ") SELECT 1 LIMIT 0"
|
||||||
let tokens = qlex.tokenize(validateSql)
|
let tokens = qlex.tokenize(validateSql)
|
||||||
let astNode = qpar.parse(tokens)
|
let astNode = qpar.parse(tokens)
|
||||||
if astNode.stmts.len > 0:
|
if astNode.stmts.len > 0:
|
||||||
@@ -1349,7 +1398,6 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
var correctedSql = llmmod.extractSQL(correctedResponse)
|
var correctedSql = llmmod.extractSQL(correctedResponse)
|
||||||
if correctedSql.len > 0:
|
if correctedSql.len > 0:
|
||||||
return correctedSql
|
return correctedSql
|
||||||
|
|
||||||
return sql
|
return sql
|
||||||
of "schema_prompt":
|
of "schema_prompt":
|
||||||
if expr.irFuncArgs.len < 1: return ""
|
if expr.irFuncArgs.len < 1: return ""
|
||||||
@@ -1358,33 +1406,33 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
return "Table '" & table & "' not found"
|
return "Table '" & table & "' not found"
|
||||||
|
|
||||||
let tbl = ctx.tables[table]
|
let tbl = ctx.tables[table]
|
||||||
var result = ""
|
var ddl = ""
|
||||||
result.add("CREATE TABLE " & table & " (\n")
|
ddl.add("CREATE TABLE " & table & " (\n")
|
||||||
for i, col in tbl.columns:
|
for i, col in tbl.columns:
|
||||||
result.add(" " & col.name & " " & col.colType)
|
ddl.add(" " & col.name & " " & col.colType)
|
||||||
if col.isPk: result.add(" PRIMARY KEY")
|
if col.isPk: ddl.add(" PRIMARY KEY")
|
||||||
if col.isNotNull: result.add(" NOT NULL")
|
if col.isNotNull: ddl.add(" NOT NULL")
|
||||||
if col.autoIncrement: result.add(" AUTO_INCREMENT")
|
if col.autoIncrement: ddl.add(" AUTO_INCREMENT")
|
||||||
if col.fkTable.len > 0:
|
if col.fkTable.len > 0:
|
||||||
result.add(" REFERENCES " & col.fkTable & "(" & col.fkColumn & ")")
|
ddl.add(" REFERENCES " & col.fkTable & "(" & col.fkColumn & ")")
|
||||||
if i < tbl.columns.len - 1: result.add(",")
|
if i < tbl.columns.len - 1: ddl.add(",")
|
||||||
result.add("\n")
|
ddl.add("\n")
|
||||||
|
|
||||||
# Sample data
|
# Sample data
|
||||||
var kvPairs: seq[(string, seq[byte])] = @[]
|
var kvPairs: seq[(string, seq[byte])] = @[]
|
||||||
let rows = execScan(ctx, table)
|
let rows = execScan(ctx, table)
|
||||||
let sampleLimit = min(5, rows.len)
|
let sampleLimit = min(5, rows.len)
|
||||||
if sampleLimit > 0:
|
if sampleLimit > 0:
|
||||||
result.add(");\n\n-- Sample data:\n")
|
ddl.add(");\n\n-- Sample data:\n")
|
||||||
for i in 0..<sampleLimit:
|
for i in 0..<sampleLimit:
|
||||||
result.add("-- ")
|
ddl.add("-- ")
|
||||||
var parts: seq[string] = @[]
|
var parts: seq[string] = @[]
|
||||||
for col in tbl.columns:
|
for col in tbl.columns:
|
||||||
parts.add(col.name & "=" & rows[i].getOrDefault(col.name, ""))
|
parts.add(col.name & "=" & rows[i].getOrDefault(col.name, ""))
|
||||||
result.add(parts.join(", "))
|
ddl.add(parts.join(", "))
|
||||||
result.add("\n")
|
ddl.add("\n")
|
||||||
else:
|
else:
|
||||||
result.add(");")
|
ddl.add(");")
|
||||||
|
|
||||||
# Indexes
|
# Indexes
|
||||||
var idxList: seq[string] = @[]
|
var idxList: seq[string] = @[]
|
||||||
@@ -1395,20 +1443,20 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
if idxKey.startsWith(table & "."):
|
if idxKey.startsWith(table & "."):
|
||||||
idxList.add("HNSW: " & idxKey)
|
idxList.add("HNSW: " & idxKey)
|
||||||
if idxList.len > 0:
|
if idxList.len > 0:
|
||||||
result.add("\n-- Indexes: " & idxList.join(", "))
|
ddl.add("\n-- Indexes: " & idxList.join(", "))
|
||||||
|
|
||||||
# RLS policies
|
# RLS policies
|
||||||
if table in ctx.policies and ctx.policies[table].len > 0:
|
if table in ctx.policies and ctx.policies[table].len > 0:
|
||||||
result.add("\n-- RLS Policies:\n")
|
ddl.add("\n-- RLS Policies:\n")
|
||||||
for pol in ctx.policies[table]:
|
for pol in ctx.policies[table]:
|
||||||
result.add("-- CREATE POLICY " & pol.name & " FOR " & pol.command & "\n")
|
ddl.add("-- CREATE POLICY " & pol.name & " FOR " & pol.command & "\n")
|
||||||
|
|
||||||
if tbl.foreignKeys.len > 0:
|
if tbl.foreignKeys.len > 0:
|
||||||
result.add("\n-- Foreign Keys:\n")
|
ddl.add("\n-- Foreign Keys:\n")
|
||||||
for fk in tbl.foreignKeys:
|
for fk in tbl.foreignKeys:
|
||||||
result.add("-- " & fk.refTable & "(" & fk.refColumn & ") ON DELETE " & fk.onDelete & "\n")
|
ddl.add("-- " & fk.refTable & "(" & fk.refColumn & ") ON DELETE " & fk.onDelete & "\n")
|
||||||
|
|
||||||
return result
|
return ddl
|
||||||
of "similarity_nodes":
|
of "similarity_nodes":
|
||||||
if expr.irFuncArgs.len < 1: return "[]"
|
if expr.irFuncArgs.len < 1: return "[]"
|
||||||
let graphName = evalExpr(expr.irFuncArgs[0], row, ctx)
|
let graphName = evalExpr(expr.irFuncArgs[0], row, ctx)
|
||||||
@@ -1508,7 +1556,6 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
finally:
|
finally:
|
||||||
release(ctx.sharedLock.lock)
|
release(ctx.sharedLock.lock)
|
||||||
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)
|
||||||
var nodeId: int64 = 0
|
var nodeId: int64 = 0
|
||||||
@@ -1662,19 +1709,29 @@ proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], valu
|
|||||||
kvPairs: var seq[(string, seq[byte])]): int =
|
kvPairs: var seq[(string, seq[byte])]): int =
|
||||||
if not hasPrivilege(ctx, table, "INSERT"):
|
if not hasPrivilege(ctx, table, "INSERT"):
|
||||||
return 0
|
return 0
|
||||||
|
let tblDef = if table in ctx.tables: ctx.tables[table] else: TableDef()
|
||||||
var count = 0
|
var count = 0
|
||||||
for rowVals in values:
|
for rowVals in values:
|
||||||
var key = ""
|
var key = ""
|
||||||
var keyFound = false
|
var keyFound = false
|
||||||
var valParts: seq[string] = @[]
|
var valParts: seq[string] = @[]
|
||||||
|
# Build composite PK key from all PK columns
|
||||||
|
if tblDef.pkColumns.len > 0:
|
||||||
|
var pkParts: seq[string] = @[]
|
||||||
|
for pkCol in tblDef.pkColumns:
|
||||||
|
let pkVal = getValue(rowVals, fields, pkCol)
|
||||||
|
pkParts.add(pkCol & "=" & escapeRowVal(pkVal))
|
||||||
|
key = pkParts.join(":")
|
||||||
|
keyFound = true
|
||||||
for i, f in fields:
|
for i, f in fields:
|
||||||
if i < rowVals.len:
|
if i < rowVals.len:
|
||||||
if not keyFound:
|
if not keyFound:
|
||||||
key = f & "=" & escapeRowVal(rowVals[i])
|
key = f & "=" & escapeRowVal(rowVals[i])
|
||||||
keyFound = true
|
keyFound = true
|
||||||
else:
|
elif tblDef.pkColumns.len == 0 or f.toLower() notin tblDef.pkColumns.mapIt(it.toLower()):
|
||||||
valParts.add(f & "=" & escapeRowVal(rowVals[i]))
|
valParts.add(f & "=" & escapeRowVal(rowVals[i]))
|
||||||
elif f.len > 0:
|
elif f.len > 0:
|
||||||
|
if tblDef.pkColumns.len == 0 or f.toLower() notin tblDef.pkColumns.mapIt(it.toLower()):
|
||||||
valParts.add(f & "=")
|
valParts.add(f & "=")
|
||||||
let valStr = valParts.join(",")
|
let valStr = valParts.join(",")
|
||||||
let fullKey = table & "." & key
|
let fullKey = table & "." & key
|
||||||
@@ -2107,32 +2164,24 @@ proc validateConstraints*(ctx: ExecutionContext, tableName: string,
|
|||||||
if not typeOk:
|
if not typeOk:
|
||||||
return (false, typeErr)
|
return (false, typeErr)
|
||||||
|
|
||||||
# FK check
|
# FK check — uses LSM get which searches memtable + SSTables
|
||||||
if col.fkTable.len > 0 and col.fkColumn.len > 0 and not isNull(val):
|
if col.fkTable.len > 0 and col.fkColumn.len > 0 and not isNull(val):
|
||||||
let fkKey = col.fkTable & "." & col.fkColumn & "=" & val
|
let fkKey = col.fkTable & "." & col.fkColumn & "=" & val
|
||||||
let (fkExists, _) = ctx.db.get(fkKey)
|
let (fkExists, _) = ctx.db.get(fkKey)
|
||||||
if not fkExists:
|
if not fkExists:
|
||||||
# Also check if value is in any row's first field
|
|
||||||
var found = false
|
|
||||||
let prefix = col.fkTable & "."
|
|
||||||
for entry in ctx.db.scanMemTable():
|
|
||||||
if entry.deleted: continue
|
|
||||||
if entry.key.startsWith(prefix):
|
|
||||||
let rest = entry.key[prefix.len..^1]
|
|
||||||
if rest.startsWith(col.fkColumn & "=") and rest[col.fkColumn.len+1..^1] == val:
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
return (false, "FOREIGN KEY violation: '" & val & "' not found in " & col.fkTable & "." & col.fkColumn)
|
return (false, "FOREIGN KEY violation: '" & val & "' not found in " & col.fkTable & "." & col.fkColumn)
|
||||||
|
|
||||||
# PK uniqueness (skip during UPDATE — PK shouldn't change)
|
# PK uniqueness (skip during UPDATE — PK shouldn't change)
|
||||||
if not skipPkCheck and tbl.pkColumns.len > 0:
|
if not skipPkCheck and tbl.pkColumns.len > 0:
|
||||||
var pkVals: seq[string] = @[]
|
var pkVals: seq[string] = @[]
|
||||||
|
var pkParts: seq[string] = @[]
|
||||||
for pkCol in tbl.pkColumns:
|
for pkCol in tbl.pkColumns:
|
||||||
pkVals.add(getValue(rowVals, fields, pkCol))
|
let pkVal = getValue(rowVals, fields, pkCol)
|
||||||
|
pkVals.add(pkVal)
|
||||||
|
pkParts.add(pkCol & "=" & escapeRowVal(pkVal))
|
||||||
let pkStr = pkVals.join("|")
|
let pkStr = pkVals.join("|")
|
||||||
# Check with field=value format (as stored by execInsert)
|
# Check with composite PK format (as stored by execInsert)
|
||||||
var pkKey = tableName & "." & tbl.pkColumns[0] & "=" & escapeRowVal(pkVals[0])
|
let pkKey = tableName & "." & pkParts.join(":")
|
||||||
let (exists, _) = ctx.db.get(pkKey)
|
let (exists, _) = ctx.db.get(pkKey)
|
||||||
if exists:
|
if exists:
|
||||||
return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "'")
|
return (false, "UNIQUE constraint violated: duplicate key '" & pkStr & "'")
|
||||||
@@ -4739,7 +4788,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
|||||||
ctx.views[stmt.cvName] = stmt.cvQuery
|
ctx.views[stmt.cvName] = stmt.cvQuery
|
||||||
let viewKey = "_schema:views:" & stmt.cvName
|
let viewKey = "_schema:views:" & stmt.cvName
|
||||||
let viewSql = selectToSql(stmt.cvQuery)
|
let viewSql = selectToSql(stmt.cvQuery)
|
||||||
let viewDdl = "CREATE VIEW " & stmt.cvName & " AS " & viewSql
|
let viewDdl = "CREATE VIEW \"" & sqlEscapeIdent(stmt.cvName) & "\" AS " & viewSql
|
||||||
ctx.db.put(viewKey, cast[seq[byte]](viewDdl))
|
ctx.db.put(viewKey, cast[seq[byte]](viewDdl))
|
||||||
return okResult(msg="CREATE VIEW " & stmt.cvName)
|
return okResult(msg="CREATE VIEW " & stmt.cvName)
|
||||||
|
|
||||||
@@ -4762,7 +4811,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
|||||||
ctx.tables[stmt.trigTable].triggers = triggers
|
ctx.tables[stmt.trigTable].triggers = triggers
|
||||||
# Persist trigger to LSM-Tree
|
# Persist trigger to LSM-Tree
|
||||||
let trigKey = "_schema:triggers:" & stmt.trigTable & ":" & stmt.trigName
|
let trigKey = "_schema:triggers:" & stmt.trigTable & ":" & stmt.trigName
|
||||||
let trigDdl = "CREATE TRIGGER " & stmt.trigName & " ON " & stmt.trigTable & " " &
|
let trigDdl = "CREATE TRIGGER \"" & sqlEscapeIdent(stmt.trigName) & "\" ON \"" & sqlEscapeIdent(stmt.trigTable) & "\" " &
|
||||||
stmt.trigTiming & " " & stmt.trigEvent & " AS " & stmt.trigAction.strVal
|
stmt.trigTiming & " " & stmt.trigEvent & " AS " & stmt.trigAction.strVal
|
||||||
ctx.db.put(trigKey, cast[seq[byte]](trigDdl))
|
ctx.db.put(trigKey, cast[seq[byte]](trigDdl))
|
||||||
return okResult(msg="CREATE TRIGGER " & stmt.trigName)
|
return okResult(msg="CREATE TRIGGER " & stmt.trigName)
|
||||||
@@ -5031,7 +5080,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
|||||||
ctx.users[stmt.cuName] = UserDef(name: stmt.cuName, passwordHash: stmt.cuPassword,
|
ctx.users[stmt.cuName] = UserDef(name: stmt.cuName, passwordHash: stmt.cuPassword,
|
||||||
isSuperuser: stmt.cuSuperuser, roles: @[])
|
isSuperuser: stmt.cuSuperuser, roles: @[])
|
||||||
let userKey = "_schema:users:" & stmt.cuName
|
let userKey = "_schema:users:" & stmt.cuName
|
||||||
let userDdl = "CREATE USER " & stmt.cuName & " WITH PASSWORD '" & stmt.cuPassword & "'" &
|
let userDdl = "CREATE USER \"" & sqlEscapeIdent(stmt.cuName) & "\" WITH PASSWORD '" & sqlEscapeString(stmt.cuPassword) & "'" &
|
||||||
(if stmt.cuSuperuser: " SUPERUSER" else: " NOSUPERUSER")
|
(if stmt.cuSuperuser: " SUPERUSER" else: " NOSUPERUSER")
|
||||||
ctx.db.put(userKey, cast[seq[byte]](userDdl))
|
ctx.db.put(userKey, cast[seq[byte]](userDdl))
|
||||||
return okResult(msg="CREATE USER " & stmt.cuName)
|
return okResult(msg="CREATE USER " & stmt.cuName)
|
||||||
@@ -5050,7 +5099,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
|||||||
withCheckExpr: stmt.cpWithCheck))
|
withCheckExpr: stmt.cpWithCheck))
|
||||||
ctx.policies[stmt.cpTable] = pols
|
ctx.policies[stmt.cpTable] = pols
|
||||||
let polKey = "_schema:policies:" & stmt.cpTable & ":" & stmt.cpName
|
let polKey = "_schema:policies:" & stmt.cpTable & ":" & stmt.cpName
|
||||||
var polDdl = "CREATE POLICY " & stmt.cpName & " ON " & stmt.cpTable
|
var polDdl = "CREATE POLICY \"" & sqlEscapeIdent(stmt.cpName) & "\" ON \"" & sqlEscapeIdent(stmt.cpTable) & "\""
|
||||||
if stmt.cpCommand != "ALL":
|
if stmt.cpCommand != "ALL":
|
||||||
polDdl.add(" FOR " & stmt.cpCommand)
|
polDdl.add(" FOR " & stmt.cpCommand)
|
||||||
if stmt.cpUsing != nil:
|
if stmt.cpUsing != nil:
|
||||||
|
|||||||
@@ -181,8 +181,8 @@ proc parsePrimary(p: var Parser): Node =
|
|||||||
Node(kind: nkCase, caseExpr: caseExpr, caseWhens: whens, caseElse: elseExpr,
|
Node(kind: nkCase, caseExpr: caseExpr, caseWhens: whens, caseElse: elseExpr,
|
||||||
line: tok.line, col: tok.col)
|
line: tok.line, col: tok.col)
|
||||||
else:
|
else:
|
||||||
discard p.advance()
|
raise newException(ValueError,
|
||||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
"Unexpected token " & $tok.kind & " at line " & $tok.line & ", col " & $tok.col)
|
||||||
|
|
||||||
proc parseOverClause(p: var Parser): Node =
|
proc parseOverClause(p: var Parser): Node =
|
||||||
## Parse OVER ( [PARTITION BY expr, ...] [ORDER BY expr [ASC|DESC], ...] [frame] )
|
## Parse OVER ( [PARTITION BY expr, ...] [ORDER BY expr [ASC|DESC], ...] [frame] )
|
||||||
@@ -324,7 +324,10 @@ proc parseComparison(p: var Parser): Node =
|
|||||||
if p.peek().kind == tkNot:
|
if p.peek().kind == tkNot:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
negated = true
|
negated = true
|
||||||
discard p.advance() # consume NULL token (assumed)
|
if p.peek().kind != tkNull:
|
||||||
|
raise newException(ValueError,
|
||||||
|
"Expected NULL after IS " & (if negated: "NOT " else: "") & "at line " & $tok.line)
|
||||||
|
discard p.advance() # consume NULL token
|
||||||
return Node(kind: nkIsExpr, isExpr: result, isNegated: negated,
|
return Node(kind: nkIsExpr, isExpr: result, isNegated: negated,
|
||||||
line: tok.line, col: tok.col)
|
line: tok.line, col: tok.col)
|
||||||
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch, tkDistanceOp, tkJsonContains, tkJsonContainedBy, tkJsonHasAny, tkJsonHasAll}:
|
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch, tkDistanceOp, tkJsonContains, tkJsonContainedBy, tkJsonHasAny, tkJsonHasAll}:
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ proc deserialize*(bf: var BloomFilter, data: seq[byte]) =
|
|||||||
)
|
)
|
||||||
let numBytes = (bf.size + 7) div 8
|
let numBytes = (bf.size + 7) div 8
|
||||||
if data.len < 8 + numBytes:
|
if data.len < 8 + numBytes:
|
||||||
return
|
raise newException(ValueError, "Bloom filter data too short: expected " & $(8 + numBytes) & " bytes, got " & $data.len)
|
||||||
for i in 0..<bf.size:
|
for i in 0..<bf.size:
|
||||||
if (data[8 + i div 8] and (1'u8 shl (i mod 8))) != 0:
|
if (data[8 + i div 8] and (1'u8 shl (i mod 8))) != 0:
|
||||||
bf.bits[i] = true
|
bf.bits[i] = true
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
@@ -62,9 +62,12 @@ proc splitChild[K, V](parent: BTreeNode[K, V], index: int, order: int) =
|
|||||||
let midKey = child.keys[mid]
|
let midKey = child.keys[mid]
|
||||||
parent.keys.insert(midKey, index)
|
parent.keys.insert(midKey, index)
|
||||||
parent.children.insert(newNode, index + 1)
|
parent.children.insert(newNode, index + 1)
|
||||||
child.keys.setLen(mid)
|
# In B+ tree, leaf nodes must keep the boundary key for range scans
|
||||||
if child.isLeaf:
|
if child.isLeaf:
|
||||||
child.values.setLen(mid)
|
child.keys.setLen(mid + 1)
|
||||||
|
child.values.setLen(mid + 1)
|
||||||
|
else:
|
||||||
|
child.keys.setLen(mid)
|
||||||
|
|
||||||
proc insertNonFull[K, V](node: BTreeNode[K, V], key: K, value: V, order: int) =
|
proc insertNonFull[K, V](node: BTreeNode[K, V], key: K, value: V, order: int) =
|
||||||
var i = node.keys.len - 1
|
var i = node.keys.len - 1
|
||||||
|
|||||||
@@ -98,10 +98,9 @@ proc compact*(cs: CompactionStrategy, level: int): CompactionResult =
|
|||||||
merged.add(entry)
|
merged.add(entry)
|
||||||
lastKey = entry.key
|
lastKey = entry.key
|
||||||
|
|
||||||
# Filter out tombstones (deleted entries)
|
# Keep tombstones to prevent deleted keys from resurrecting in lower levels
|
||||||
var final: seq[Entry] = @[]
|
var final: seq[Entry] = @[]
|
||||||
for entry in merged:
|
for entry in merged:
|
||||||
if not entry.deleted:
|
|
||||||
final.add(entry)
|
final.add(entry)
|
||||||
|
|
||||||
# Write merged SSTable
|
# Write merged SSTable
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ proc len*(mt: MemTable): int = mt.entries.len
|
|||||||
|
|
||||||
proc put*(mt: var MemTable, key: string, value: seq[byte], timestamp: uint64, deleted: bool = false): bool =
|
proc put*(mt: var MemTable, key: string, value: seq[byte], timestamp: uint64, deleted: bool = false): bool =
|
||||||
let entrySize = key.len + value.len + 16
|
let entrySize = key.len + value.len + 16
|
||||||
|
if entrySize > mt.maxSize:
|
||||||
|
return false
|
||||||
if mt.size + entrySize > mt.maxSize and mt.entries.len > 0:
|
if mt.size + entrySize > mt.maxSize and mt.entries.len > 0:
|
||||||
return false
|
return false
|
||||||
let entry = Entry(key: key, value: value, timestamp: timestamp, deleted: deleted)
|
let entry = Entry(key: key, value: value, timestamp: timestamp, deleted: deleted)
|
||||||
@@ -318,7 +320,7 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
|
|||||||
except:
|
except:
|
||||||
discard # skip corrupt SSTables
|
discard # skip corrupt SSTables
|
||||||
|
|
||||||
sstables.sort(proc(a, b: SSTable): int = cmp(b.id, a.id))
|
sstables.sort(proc(a, b: SSTable): int = cmp(a.id, b.id))
|
||||||
|
|
||||||
new(result)
|
new(result)
|
||||||
initLock(result.lock)
|
initLock(result.lock)
|
||||||
@@ -405,6 +407,21 @@ proc delete*(db: LSMTree, key: string) =
|
|||||||
db.memTable = newMemTable(db.memMaxSize)
|
db.memTable = newMemTable(db.memMaxSize)
|
||||||
discard db.memTable.put(key, @[], ts, deleted = true)
|
discard db.memTable.put(key, @[], ts, deleted = true)
|
||||||
|
|
||||||
|
proc putUnsafe*(db: LSMTree, key: string, value: seq[byte], deleted: bool = false) =
|
||||||
|
## Direct LSM insert without WAL logging — used by recovery.
|
||||||
|
let ts = uint64(getMonoTime().ticks())
|
||||||
|
acquire(db.lock)
|
||||||
|
defer: release(db.lock)
|
||||||
|
if not db.memTable.put(key, value, ts, deleted):
|
||||||
|
if db.immutableMem.len > 0:
|
||||||
|
db.flushUnsafe()
|
||||||
|
db.immutableMem = db.memTable
|
||||||
|
db.memTable = newMemTable(db.memMaxSize)
|
||||||
|
discard db.memTable.put(key, value, ts, deleted)
|
||||||
|
|
||||||
|
proc deleteUnsafe*(db: LSMTree, key: string) =
|
||||||
|
putUnsafe(db, key, @[], deleted = true)
|
||||||
|
|
||||||
proc getUnsafe(db: LSMTree, key: string): (bool, seq[byte]) =
|
proc getUnsafe(db: LSMTree, key: string): (bool, seq[byte]) =
|
||||||
let (found, entry) = db.memTable.get(key)
|
let (found, entry) = db.memTable.get(key)
|
||||||
if found:
|
if found:
|
||||||
@@ -478,6 +495,9 @@ proc flush*(db: LSMTree) =
|
|||||||
proc close*(db: LSMTree) =
|
proc close*(db: LSMTree) =
|
||||||
acquire(db.lock)
|
acquire(db.lock)
|
||||||
defer: release(db.lock)
|
defer: release(db.lock)
|
||||||
|
# Flush both memtables to avoid data loss
|
||||||
|
while db.immutableMem.len > 0:
|
||||||
|
flushUnsafe(db)
|
||||||
flushUnsafe(db)
|
flushUnsafe(db)
|
||||||
for sst in db.sstables.mitems:
|
for sst in db.sstables.mitems:
|
||||||
sst.close()
|
sst.close()
|
||||||
@@ -531,7 +551,8 @@ proc scanAll*(db: LSMTree): seq[(string, seq[byte])] =
|
|||||||
result.add((e.key, e.value))
|
result.add((e.key, e.value))
|
||||||
|
|
||||||
# Scan SSTables from newest to oldest
|
# Scan SSTables from newest to oldest
|
||||||
for sst in db.sstables:
|
for i in countdown(db.sstables.high, db.sstables.low):
|
||||||
|
let sst = db.sstables[i]
|
||||||
for key, offset in sst.index:
|
for key, offset in sst.index:
|
||||||
if key notin seen:
|
if key notin seen:
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
|
|||||||
@@ -54,14 +54,18 @@ proc openMmap*(path: string, mode: MmapMode = mmReadOnly): MmapFile =
|
|||||||
|
|
||||||
let mapped = mmap(nil, fileSize, prot, flags, fd, 0)
|
let mapped = mmap(nil, fileSize, prot, flags, fd, 0)
|
||||||
if mapped == MAP_FAILED:
|
if mapped == MAP_FAILED:
|
||||||
discard close(fd)
|
discard posix.close(fd)
|
||||||
return MmapFile(path: path, regions: @[], totalSize: 0, pageSize: PageSize)
|
return MmapFile(path: path, regions: @[], totalSize: 0, pageSize: PageSize)
|
||||||
|
|
||||||
|
# Kernel keeps the mapping alive independently of the fd; close it now
|
||||||
|
# to avoid leaking one fd per SSTable.
|
||||||
|
discard posix.close(fd)
|
||||||
|
|
||||||
let region = MmapRegion(
|
let region = MmapRegion(
|
||||||
data: cast[ptr UncheckedArray[byte]](mapped),
|
data: cast[ptr UncheckedArray[byte]](mapped),
|
||||||
size: fileSize,
|
size: fileSize,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
fd: fd,
|
fd: -1,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,15 +120,20 @@ proc adviseRandom*(mf: MmapFile) =
|
|||||||
|
|
||||||
proc adviseWillNeed*(mf: MmapFile, offset: int, size: int) =
|
proc adviseWillNeed*(mf: MmapFile, offset: int, size: int) =
|
||||||
if mf.regions.len > 0:
|
if mf.regions.len > 0:
|
||||||
|
if offset < 0 or offset + size > mf.regions[0].size:
|
||||||
|
return
|
||||||
discard madvise(addr mf.regions[0].data[offset], size, MADV_WILLNEED)
|
discard madvise(addr mf.regions[0].data[offset], size, MADV_WILLNEED)
|
||||||
|
|
||||||
proc adviseDontNeed*(mf: MmapFile, offset: int, size: int) =
|
proc adviseDontNeed*(mf: MmapFile, offset: int, size: int) =
|
||||||
if mf.regions.len > 0:
|
if mf.regions.len > 0:
|
||||||
|
if offset < 0 or offset + size > mf.regions[0].size:
|
||||||
|
return
|
||||||
discard madvise(addr mf.regions[0].data[offset], size, MADV_DONTNEED)
|
discard madvise(addr mf.regions[0].data[offset], size, MADV_DONTNEED)
|
||||||
|
|
||||||
proc close*(mf: MmapFile) =
|
proc close*(mf: MmapFile) =
|
||||||
for region in mf.regions:
|
for region in mf.regions:
|
||||||
discard munmap(region.data, region.size)
|
discard munmap(region.data, region.size)
|
||||||
|
if region.fd != -1:
|
||||||
discard close(cint(region.fd))
|
discard close(cint(region.fd))
|
||||||
mf.regions.setLen(0)
|
mf.regions.setLen(0)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type
|
|||||||
dataDir*: string
|
dataDir*: string
|
||||||
entries*: seq[RecoveredEntry]
|
entries*: seq[RecoveredEntry]
|
||||||
result*: RecoveryResult
|
result*: RecoveryResult
|
||||||
|
lastTxnId*: uint64 # tracks commits seen in WAL
|
||||||
|
|
||||||
proc newCrashRecovery*(walDir: string, dataDir: string): CrashRecovery =
|
proc newCrashRecovery*(walDir: string, dataDir: string): CrashRecovery =
|
||||||
CrashRecovery(
|
CrashRecovery(
|
||||||
@@ -101,6 +102,7 @@ proc scanWAL*(rec: CrashRecovery): seq[RecoveredEntry] =
|
|||||||
discard
|
discard
|
||||||
|
|
||||||
stream.close()
|
stream.close()
|
||||||
|
rec.lastTxnId = txnId
|
||||||
|
|
||||||
proc analyze*(rec: CrashRecovery): RecoveryResult =
|
proc analyze*(rec: CrashRecovery): RecoveryResult =
|
||||||
rec.entries = rec.scanWAL()
|
rec.entries = rec.scanWAL()
|
||||||
@@ -109,10 +111,7 @@ proc analyze*(rec: CrashRecovery): RecoveryResult =
|
|||||||
rec.result = RecoveryResult(state: recDone, applied: false)
|
rec.result = RecoveryResult(state: recDone, applied: false)
|
||||||
return rec.result
|
return rec.result
|
||||||
|
|
||||||
var lastCommitted: uint64 = 0
|
var lastCommitted = rec.lastTxnId
|
||||||
for entry in rec.entries:
|
|
||||||
if entry.txnId > lastCommitted:
|
|
||||||
lastCommitted = entry.txnId
|
|
||||||
|
|
||||||
var redoCount = 0
|
var redoCount = 0
|
||||||
var undoCount = 0
|
var undoCount = 0
|
||||||
@@ -149,11 +148,11 @@ proc recover*(rec: CrashRecovery, db: LSMTree = nil): RecoveryResult =
|
|||||||
var undoCount = 0
|
var undoCount = 0
|
||||||
for entry in rec.entries:
|
for entry in rec.entries:
|
||||||
if entry.txnId < analysis.lastTxn:
|
if entry.txnId < analysis.lastTxn:
|
||||||
# Committed — redo
|
# Committed — redo (bypass WAL to avoid duplicate entries)
|
||||||
if entry.isDelete:
|
if entry.isDelete:
|
||||||
db.delete(entry.key)
|
db.deleteUnsafe(entry.key)
|
||||||
else:
|
else:
|
||||||
db.put(entry.key, entry.value)
|
db.putUnsafe(entry.key, entry.value)
|
||||||
inc redoCount
|
inc redoCount
|
||||||
else:
|
else:
|
||||||
# Uncommitted — skip (undo)
|
# Uncommitted — skip (undo)
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ proc newWriteAheadLog*(dir: string, syncOnWrite: bool = true): WriteAheadLog =
|
|||||||
createDir(dir)
|
createDir(dir)
|
||||||
let path = dir / "wal.log"
|
let path = dir / "wal.log"
|
||||||
let exists = fileExists(path)
|
let exists = fileExists(path)
|
||||||
|
let isEmpty = if exists: getFileSize(path) == 0 else: true
|
||||||
let stream = if exists: newFileStream(path, fmAppend) else: newFileStream(path, fmWrite)
|
let stream = if exists: newFileStream(path, fmAppend) else: newFileStream(path, fmWrite)
|
||||||
if stream == nil:
|
if stream == nil:
|
||||||
raise newException(IOError, "Cannot open WAL: " & path)
|
raise newException(IOError, "Cannot open WAL: " & path)
|
||||||
if not exists:
|
if not exists or isEmpty:
|
||||||
stream.write(WALMagic)
|
stream.write(WALMagic)
|
||||||
stream.write(WALVersion)
|
stream.write(WALVersion)
|
||||||
stream.flush()
|
stream.flush()
|
||||||
@@ -78,14 +79,17 @@ proc writeCommit*(wal: var WriteAheadLog, timestamp: uint64) =
|
|||||||
|
|
||||||
proc sync*(wal: var WriteAheadLog) =
|
proc sync*(wal: var WriteAheadLog) =
|
||||||
wal.stream.flush()
|
wal.stream.flush()
|
||||||
let fd = posix.open(cstring(wal.path), O_RDONLY)
|
# Re-open with O_RDWR so fsync operates on a write-capable fd.
|
||||||
|
# Not ideal (two fds for same file) but avoids accessing private
|
||||||
|
# FileStream internals that vary across Nim versions.
|
||||||
|
let fd = posix.open(cstring(wal.path), O_RDWR)
|
||||||
if fd != -1:
|
if fd != -1:
|
||||||
discard posix.fsync(fd)
|
discard posix.fsync(fd)
|
||||||
discard posix.close(fd)
|
discard posix.close(fd)
|
||||||
|
|
||||||
proc close*(wal: var WriteAheadLog) =
|
proc close*(wal: var WriteAheadLog) =
|
||||||
wal.stream.flush()
|
wal.stream.flush()
|
||||||
let fd = posix.open(cstring(wal.path), O_RDONLY)
|
let fd = posix.open(cstring(wal.path), O_RDWR)
|
||||||
if fd != -1:
|
if fd != -1:
|
||||||
discard posix.fsync(fd)
|
discard posix.fsync(fd)
|
||||||
discard posix.close(fd)
|
discard posix.close(fd)
|
||||||
@@ -100,10 +104,10 @@ proc readEntries*(walPath: string, untilTimestamp: uint64 = 0): seq[WalEntry] =
|
|||||||
let s = newFileStream(walPath, fmRead)
|
let s = newFileStream(walPath, fmRead)
|
||||||
if s == nil: return
|
if s == nil: return
|
||||||
# Skip header
|
# Skip header
|
||||||
var magic: uint32
|
var magic: uint32 = 0
|
||||||
var version: uint32
|
var version: uint32 = 0
|
||||||
discard s.readData(addr magic, 4)
|
if s.readData(addr magic, 4) != 4: return
|
||||||
discard s.readData(addr version, 4)
|
if s.readData(addr version, 4) != 4: return
|
||||||
if magic != WALMagic: return
|
if magic != WALMagic: return
|
||||||
while not s.atEnd:
|
while not s.atEnd:
|
||||||
var kind: uint8
|
var kind: uint8
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -3845,6 +3845,59 @@ suite "Foreign Key Enforcement":
|
|||||||
check childSel.rows.len == 0
|
check childSel.rows.len == 0
|
||||||
|
|
||||||
|
|
||||||
|
suite "Composite Primary Key":
|
||||||
|
var db: LSMTree
|
||||||
|
var ctx: qexec.ExecutionContext
|
||||||
|
var tmpDir: string
|
||||||
|
|
||||||
|
setup:
|
||||||
|
tmpDir = getTempDir() / "baradb_compk_test_" & $getMonoTime().ticks
|
||||||
|
db = newLSMTree(tmpDir)
|
||||||
|
ctx = qexec.newExecutionContext(db)
|
||||||
|
|
||||||
|
teardown:
|
||||||
|
removeDir(tmpDir)
|
||||||
|
|
||||||
|
test "Composite PK insert stores distinct rows":
|
||||||
|
discard qexec.executeQuery(ctx, parse("CREATE TABLE orders (user_id INT, order_id INT, amount INT, PRIMARY KEY (user_id, order_id))"))
|
||||||
|
discard qexec.executeQuery(ctx, parse("INSERT INTO orders (user_id, order_id, amount) VALUES (1, 100, 50)"))
|
||||||
|
discard qexec.executeQuery(ctx, parse("INSERT INTO orders (user_id, order_id, amount) VALUES (1, 101, 75)"))
|
||||||
|
let sel = qexec.executeQuery(ctx, parse("SELECT * FROM orders"))
|
||||||
|
check sel.success
|
||||||
|
check sel.rows.len == 2
|
||||||
|
|
||||||
|
test "Composite PK prevents duplicate key":
|
||||||
|
discard qexec.executeQuery(ctx, parse("CREATE TABLE items (warehouse INT, sku TEXT, qty INT, PRIMARY KEY (warehouse, sku))"))
|
||||||
|
discard qexec.executeQuery(ctx, parse("INSERT INTO items (warehouse, sku, qty) VALUES (1, 'ABC', 10)"))
|
||||||
|
let dup = qexec.executeQuery(ctx, parse("INSERT INTO items (warehouse, sku, qty) VALUES (1, 'ABC', 20)"))
|
||||||
|
check not dup.success
|
||||||
|
check dup.message.contains("UNIQUE constraint violated")
|
||||||
|
|
||||||
|
test "Composite PK allows same first column with different second":
|
||||||
|
discard qexec.executeQuery(ctx, parse("CREATE TABLE scores (player INT, level INT, score INT, PRIMARY KEY (player, level))"))
|
||||||
|
discard qexec.executeQuery(ctx, parse("INSERT INTO scores (player, level, score) VALUES (1, 1, 100)"))
|
||||||
|
discard qexec.executeQuery(ctx, parse("INSERT INTO scores (player, level, score) VALUES (1, 2, 200)"))
|
||||||
|
let sel = qexec.executeQuery(ctx, parse("SELECT * FROM scores"))
|
||||||
|
check sel.success
|
||||||
|
check sel.rows.len == 2
|
||||||
|
|
||||||
|
test "Composite PK SELECT returns correct rows":
|
||||||
|
discard qexec.executeQuery(ctx, parse("CREATE TABLE pair (a INT, b INT, val TEXT, PRIMARY KEY (a, b))"))
|
||||||
|
discard qexec.executeQuery(ctx, parse("INSERT INTO pair (a, b, val) VALUES (1, 2, 'first')"))
|
||||||
|
discard qexec.executeQuery(ctx, parse("INSERT INTO pair (a, b, val) VALUES (1, 3, 'second')"))
|
||||||
|
let sel = qexec.executeQuery(ctx, parse("SELECT * FROM pair"))
|
||||||
|
check sel.success
|
||||||
|
check sel.rows.len == 2
|
||||||
|
# Verify both rows exist (order may vary)
|
||||||
|
var foundFirst = false
|
||||||
|
var foundSecond = false
|
||||||
|
for row in sel.rows:
|
||||||
|
if row["val"] == "first": foundFirst = true
|
||||||
|
if row["val"] == "second": foundSecond = true
|
||||||
|
check foundFirst
|
||||||
|
check foundSecond
|
||||||
|
|
||||||
|
|
||||||
suite "Hybrid RAG Search":
|
suite "Hybrid RAG Search":
|
||||||
var db: LSMTree
|
var db: LSMTree
|
||||||
var ctx: qexec.ExecutionContext
|
var ctx: qexec.ExecutionContext
|
||||||
|
|||||||
Reference in New Issue
Block a user