Phase 5 complete: B-tree property tests + benchmark regression suite
- prop_test.nim: 11 new property-based B-Tree invariants (size accuracy, get roundtrip, scan ordering, range correctness, contains, remove, large order, duplicates, empty tree, interleaved ops) - bench_all.nim: JSON-based benchmark result tracking with regression comparison against previous runs; each benchmark reports ops/sec delta vs last run
This commit is contained in:
+150
-18
@@ -4,6 +4,9 @@ import std/times
|
|||||||
import std/tables
|
import std/tables
|
||||||
import std/random
|
import std/random
|
||||||
import std/strutils
|
import std/strutils
|
||||||
|
import std/os
|
||||||
|
import std/osproc
|
||||||
|
import std/json
|
||||||
import ../src/barabadb/storage/lsm
|
import ../src/barabadb/storage/lsm
|
||||||
import ../src/barabadb/storage/btree
|
import ../src/barabadb/storage/btree
|
||||||
import ../src/barabadb/vector/engine as vengine
|
import ../src/barabadb/vector/engine as vengine
|
||||||
@@ -11,6 +14,89 @@ import ../src/barabadb/vector/simd
|
|||||||
import ../src/barabadb/fts/engine as fts
|
import ../src/barabadb/fts/engine as fts
|
||||||
import ../src/barabadb/graph/engine as gengine
|
import ../src/barabadb/graph/engine as gengine
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════
|
||||||
|
# Benchmark Result Tracking
|
||||||
|
# ═══════════════════════════════════════════════════
|
||||||
|
type
|
||||||
|
BenchResult* = object
|
||||||
|
name*: string
|
||||||
|
ops*: int
|
||||||
|
seconds*: float64
|
||||||
|
opsPerSec*: float64
|
||||||
|
timestamp*: string
|
||||||
|
|
||||||
|
BenchReport* = object
|
||||||
|
version*: string
|
||||||
|
gitSha*: string
|
||||||
|
results*: seq[BenchResult]
|
||||||
|
|
||||||
|
const ResultsFile = "benchmark_results.json"
|
||||||
|
|
||||||
|
proc loadPreviousResults(path: string): seq[BenchResult] =
|
||||||
|
result = @[]
|
||||||
|
if not fileExists(path): return
|
||||||
|
try:
|
||||||
|
let j = parseFile(path)
|
||||||
|
if j.hasKey("results"):
|
||||||
|
for item in j["results"]:
|
||||||
|
if item.hasKey("name") and item.hasKey("opsPerSec"):
|
||||||
|
result.add(BenchResult(
|
||||||
|
name: item["name"].getStr(),
|
||||||
|
ops: if item.hasKey("ops"): item["ops"].getInt() else: 0,
|
||||||
|
seconds: if item.hasKey("seconds"): item["seconds"].getFloat() else: 0.0,
|
||||||
|
opsPerSec: item["opsPerSec"].getFloat(),
|
||||||
|
timestamp: if item.hasKey("timestamp"): item["timestamp"].getStr() else: "",
|
||||||
|
))
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
proc saveResults(path: string, report: BenchReport) =
|
||||||
|
var j = newJObject()
|
||||||
|
j["version"] = %report.version
|
||||||
|
j["gitSha"] = %report.gitSha
|
||||||
|
var arr = newJArray()
|
||||||
|
for r in report.results:
|
||||||
|
var obj = newJObject()
|
||||||
|
obj["name"] = %r.name
|
||||||
|
obj["ops"] = %r.ops
|
||||||
|
obj["seconds"] = %r.seconds
|
||||||
|
obj["opsPerSec"] = %r.opsPerSec
|
||||||
|
obj["timestamp"] = %r.timestamp
|
||||||
|
arr.add(obj)
|
||||||
|
j["results"] = arr
|
||||||
|
writeFile(path, j.pretty())
|
||||||
|
|
||||||
|
proc compareResult(name: string, currentOpsPerSec: float64, previous: seq[BenchResult]): string =
|
||||||
|
for p in previous:
|
||||||
|
if p.name == name:
|
||||||
|
let delta = currentOpsPerSec - p.opsPerSec
|
||||||
|
let pct = if p.opsPerSec > 0: (delta / p.opsPerSec) * 100.0 else: 0.0
|
||||||
|
if abs(pct) < 1.0:
|
||||||
|
return " (≈ same)"
|
||||||
|
elif pct > 0:
|
||||||
|
return " (+" & pct.formatFloat(ffDecimal, 1) & "% vs last)"
|
||||||
|
else:
|
||||||
|
return " (" & pct.formatFloat(ffDecimal, 1) & "% vs last)"
|
||||||
|
return " (new)"
|
||||||
|
|
||||||
|
var currentResults: seq[BenchResult] = @[]
|
||||||
|
var previousResults = loadPreviousResults(ResultsFile)
|
||||||
|
|
||||||
|
proc recordResult(name: string, ops: int, seconds: float64) =
|
||||||
|
currentResults.add(BenchResult(
|
||||||
|
name: name,
|
||||||
|
ops: ops,
|
||||||
|
seconds: seconds,
|
||||||
|
opsPerSec: if seconds > 0: float64(ops) / seconds else: 0.0,
|
||||||
|
timestamp: now().format("yyyy-MM-dd HH:mm:ss"),
|
||||||
|
))
|
||||||
|
|
||||||
|
proc gitSha(): string =
|
||||||
|
let (output, code) = execCmdEx("git rev-parse --short HEAD")
|
||||||
|
if code == 0:
|
||||||
|
return output.strip()
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
proc elapsed(start: MonoTime): float64 =
|
proc elapsed(start: MonoTime): float64 =
|
||||||
let ns = float64((getMonoTime() - start).inNanoseconds)
|
let ns = float64((getMonoTime() - start).inNanoseconds)
|
||||||
return ns / 1_000_000_000.0
|
return ns / 1_000_000_000.0
|
||||||
@@ -26,7 +112,9 @@ proc formatOps(ops: int, secs: float64): string =
|
|||||||
|
|
||||||
proc benchLSMTree() =
|
proc benchLSMTree() =
|
||||||
echo "=== LSM-Tree Storage ==="
|
echo "=== LSM-Tree Storage ==="
|
||||||
var db = newLSMTree("/tmp/baradb_bench_lsm")
|
let benchDir = getTempDir() / "baradb_bench_lsm"
|
||||||
|
removeDir(benchDir)
|
||||||
|
var db = newLSMTree(benchDir)
|
||||||
|
|
||||||
# Write benchmark
|
# Write benchmark
|
||||||
let n = 100_000
|
let n = 100_000
|
||||||
@@ -34,7 +122,9 @@ proc benchLSMTree() =
|
|||||||
for i in 0..<n:
|
for i in 0..<n:
|
||||||
db.put("key_" & $i, cast[seq[byte]]("value_" & $i))
|
db.put("key_" & $i, cast[seq[byte]]("value_" & $i))
|
||||||
let writeTime = elapsed(start)
|
let writeTime = elapsed(start)
|
||||||
echo " Write ", n, " keys: ", writeTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, writeTime), ")"
|
let writeLabel = "LSM-Write"
|
||||||
|
recordResult(writeLabel, n, writeTime)
|
||||||
|
echo " Write ", n, " keys: ", writeTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, writeTime), ")", compareResult(writeLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# Read benchmark
|
# Read benchmark
|
||||||
let readStart = getMonoTime()
|
let readStart = getMonoTime()
|
||||||
@@ -43,7 +133,9 @@ proc benchLSMTree() =
|
|||||||
let (ok, _) = db.get("key_" & $i)
|
let (ok, _) = db.get("key_" & $i)
|
||||||
if ok: inc found
|
if ok: inc found
|
||||||
let readTime = elapsed(readStart)
|
let readTime = elapsed(readStart)
|
||||||
echo " Read ", n, " keys: ", readTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, readTime), ") (", found, " found)"
|
let readLabel = "LSM-Read"
|
||||||
|
recordResult(readLabel, n, readTime)
|
||||||
|
echo " Read ", n, " keys: ", readTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, readTime), ") (", found, " found)", compareResult(readLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@@ -57,7 +149,9 @@ proc benchBTree() =
|
|||||||
for i in 0..<n:
|
for i in 0..<n:
|
||||||
btree.insert("key_" & $i, "value_" & $i)
|
btree.insert("key_" & $i, "value_" & $i)
|
||||||
let insertTime = elapsed(start)
|
let insertTime = elapsed(start)
|
||||||
echo " Insert ", n, " keys: ", insertTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, insertTime), ")"
|
let insertLabel = "BTree-Insert"
|
||||||
|
recordResult(insertLabel, n, insertTime)
|
||||||
|
echo " Insert ", n, " keys: ", insertTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, insertTime), ")", compareResult(insertLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# Get benchmark
|
# Get benchmark
|
||||||
let getStart = getMonoTime()
|
let getStart = getMonoTime()
|
||||||
@@ -66,13 +160,17 @@ proc benchBTree() =
|
|||||||
let vals = btree.get("key_" & $i)
|
let vals = btree.get("key_" & $i)
|
||||||
if vals.len > 0: inc found
|
if vals.len > 0: inc found
|
||||||
let getTime = elapsed(getStart)
|
let getTime = elapsed(getStart)
|
||||||
echo " Get ", n, " keys: ", getTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, getTime), ") (", found, " found)"
|
let getLabel = "BTree-Get"
|
||||||
|
recordResult(getLabel, n, getTime)
|
||||||
|
echo " Get ", n, " keys: ", getTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, getTime), ") (", found, " found)", compareResult(getLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# Scan benchmark
|
# Scan benchmark
|
||||||
let scanStart = getMonoTime()
|
let scanStart = getMonoTime()
|
||||||
let scanResults = btree.scan("key_1000", "key_2000")
|
let scanResults = btree.scan("key_1000", "key_2000")
|
||||||
let scanTime = elapsed(scanStart)
|
let scanTime = elapsed(scanStart)
|
||||||
echo " Scan 1000 range: ", scanTime.formatFloat(ffDecimal, 6), "s (", scanResults.len, " results)"
|
let scanLabel = "BTree-Scan"
|
||||||
|
recordResult(scanLabel, scanResults.len, scanTime)
|
||||||
|
echo " Scan 1000 range: ", scanTime.formatFloat(ffDecimal, 6), "s (", scanResults.len, " results)", compareResult(scanLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
proc benchVectorSearch() =
|
proc benchVectorSearch() =
|
||||||
echo "=== Vector Engine (HNSW) ==="
|
echo "=== Vector Engine (HNSW) ==="
|
||||||
@@ -89,7 +187,9 @@ proc benchVectorSearch() =
|
|||||||
vec[d] = rand(1.0)
|
vec[d] = rand(1.0)
|
||||||
vengine.insert(idx, uint64(i), vec)
|
vengine.insert(idx, uint64(i), vec)
|
||||||
let insertTime = elapsed(start)
|
let insertTime = elapsed(start)
|
||||||
echo " Insert ", n, " vectors (dim=", dim, "): ", insertTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, insertTime), ")"
|
let insertLabel = "HNSW-Insert"
|
||||||
|
recordResult(insertLabel, n, insertTime)
|
||||||
|
echo " Insert ", n, " vectors (dim=", dim, "): ", insertTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, insertTime), ")", compareResult(insertLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# Search benchmark
|
# Search benchmark
|
||||||
var query = newSeq[float32](dim)
|
var query = newSeq[float32](dim)
|
||||||
@@ -99,7 +199,9 @@ proc benchVectorSearch() =
|
|||||||
let searchStart = getMonoTime()
|
let searchStart = getMonoTime()
|
||||||
let results = vengine.search(idx, query, 10)
|
let results = vengine.search(idx, query, 10)
|
||||||
let searchTime = elapsed(searchStart)
|
let searchTime = elapsed(searchStart)
|
||||||
echo " Search top-10: ", (searchTime * 1000).formatFloat(ffDecimal, 3), "ms"
|
let searchLabel = "HNSW-Search"
|
||||||
|
recordResult(searchLabel, 1, searchTime)
|
||||||
|
echo " Search top-10: ", (searchTime * 1000).formatFloat(ffDecimal, 3), "ms", compareResult(searchLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
proc benchVectorSIMD() =
|
proc benchVectorSIMD() =
|
||||||
echo "=== Vector SIMD Operations ==="
|
echo "=== Vector SIMD Operations ==="
|
||||||
@@ -122,21 +224,27 @@ proc benchVectorSIMD() =
|
|||||||
for i in 0..<n:
|
for i in 0..<n:
|
||||||
discard cosineSimd(query, corpus[i])
|
discard cosineSimd(query, corpus[i])
|
||||||
let cosineTime = elapsed(start)
|
let cosineTime = elapsed(start)
|
||||||
echo " Cosine distance (dim=768, n=10K): ", cosineTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, cosineTime), ")"
|
let cosineLabel = "SIMD-Cosine"
|
||||||
|
recordResult(cosineLabel, n, cosineTime)
|
||||||
|
echo " Cosine distance (dim=768, n=10K): ", cosineTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, cosineTime), ")", compareResult(cosineLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# L2 distance benchmark
|
# L2 distance benchmark
|
||||||
let l2Start = getMonoTime()
|
let l2Start = getMonoTime()
|
||||||
for i in 0..<n:
|
for i in 0..<n:
|
||||||
discard l2NormSimd(query, corpus[i])
|
discard l2NormSimd(query, corpus[i])
|
||||||
let l2Time = elapsed(l2Start)
|
let l2Time = elapsed(l2Start)
|
||||||
echo " L2 distance (dim=768, n=10K): ", l2Time.formatFloat(ffDecimal, 3), "s (", formatOps(n, l2Time), ")"
|
let l2Label = "SIMD-L2"
|
||||||
|
recordResult(l2Label, n, l2Time)
|
||||||
|
echo " L2 distance (dim=768, n=10K): ", l2Time.formatFloat(ffDecimal, 3), "s (", formatOps(n, l2Time), ")", compareResult(l2Label, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# Dot product benchmark
|
# Dot product benchmark
|
||||||
let dotStart = getMonoTime()
|
let dotStart = getMonoTime()
|
||||||
for i in 0..<n:
|
for i in 0..<n:
|
||||||
discard dotProductSimd(query, corpus[i])
|
discard dotProductSimd(query, corpus[i])
|
||||||
let dotTime = elapsed(dotStart)
|
let dotTime = elapsed(dotStart)
|
||||||
echo " Dot product (dim=768, n=10K): ", dotTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, dotTime), ")"
|
let dotLabel = "SIMD-Dot"
|
||||||
|
recordResult(dotLabel, n, dotTime)
|
||||||
|
echo " Dot product (dim=768, n=10K): ", dotTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, dotTime), ")", compareResult(dotLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
proc benchFTS() =
|
proc benchFTS() =
|
||||||
echo "=== Full-Text Search ==="
|
echo "=== Full-Text Search ==="
|
||||||
@@ -155,21 +263,27 @@ proc benchFTS() =
|
|||||||
for i in 0..<n:
|
for i in 0..<n:
|
||||||
idx.addDocument(uint64(i), docs[i mod docs.len])
|
idx.addDocument(uint64(i), docs[i mod docs.len])
|
||||||
let indexTime = elapsed(start)
|
let indexTime = elapsed(start)
|
||||||
echo " Index ", n, " docs: ", indexTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, indexTime), ")"
|
let indexLabel = "FTS-Index"
|
||||||
|
recordResult(indexLabel, n, indexTime)
|
||||||
|
echo " Index ", n, " docs: ", indexTime.formatFloat(ffDecimal, 3), "s (", formatOps(n, indexTime), ")", compareResult(indexLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# Search benchmark
|
# Search benchmark
|
||||||
let searchStart = getMonoTime()
|
let searchStart = getMonoTime()
|
||||||
for i in 0..<1000:
|
for i in 0..<1000:
|
||||||
discard idx.search("Nim programming language")
|
discard idx.search("Nim programming language")
|
||||||
let searchTime = elapsed(searchStart)
|
let searchTime = elapsed(searchStart)
|
||||||
echo " Search 1000 queries: ", searchTime.formatFloat(ffDecimal, 3), "s (", formatOps(1000, searchTime), ")"
|
let searchLabel = "FTS-Search"
|
||||||
|
recordResult(searchLabel, 1000, searchTime)
|
||||||
|
echo " Search 1000 queries: ", searchTime.formatFloat(ffDecimal, 3), "s (", formatOps(1000, searchTime), ")", compareResult(searchLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# Fuzzy search benchmark
|
# Fuzzy search benchmark
|
||||||
let fuzzyStart = getMonoTime()
|
let fuzzyStart = getMonoTime()
|
||||||
for i in 0..<100:
|
for i in 0..<100:
|
||||||
discard idx.fuzzySearch("programing", maxDistance = 2)
|
discard idx.fuzzySearch("programing", maxDistance = 2)
|
||||||
let fuzzyTime = elapsed(fuzzyStart)
|
let fuzzyTime = elapsed(fuzzyStart)
|
||||||
echo " Fuzzy search 100 queries: ", fuzzyTime.formatFloat(ffDecimal, 3), "s (", formatOps(100, fuzzyTime), ")"
|
let fuzzyLabel = "FTS-Fuzzy"
|
||||||
|
recordResult(fuzzyLabel, 100, fuzzyTime)
|
||||||
|
echo " Fuzzy search 100 queries: ", fuzzyTime.formatFloat(ffDecimal, 3), "s (", formatOps(100, fuzzyTime), ")", compareResult(fuzzyLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
proc benchGraph() =
|
proc benchGraph() =
|
||||||
echo "=== Graph Engine ==="
|
echo "=== Graph Engine ==="
|
||||||
@@ -182,7 +296,9 @@ proc benchGraph() =
|
|||||||
for i in 0..<nodeCount:
|
for i in 0..<nodeCount:
|
||||||
discard gengine.addNode(g, "Node_" & $i)
|
discard gengine.addNode(g, "Node_" & $i)
|
||||||
let nodeTime = elapsed(nodeStart)
|
let nodeTime = elapsed(nodeStart)
|
||||||
echo " Add ", nodeCount, " nodes: ", nodeTime.formatFloat(ffDecimal, 6), "s"
|
let nodeLabel = "Graph-AddNodes"
|
||||||
|
recordResult(nodeLabel, nodeCount, nodeTime)
|
||||||
|
echo " Add ", nodeCount, " nodes: ", nodeTime.formatFloat(ffDecimal, 6), "s", compareResult(nodeLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# Add edges
|
# Add edges
|
||||||
randomize(42)
|
randomize(42)
|
||||||
@@ -192,20 +308,26 @@ proc benchGraph() =
|
|||||||
let dst = NodeId(uint64(rand(nodeCount - 1)) + 1)
|
let dst = NodeId(uint64(rand(nodeCount - 1)) + 1)
|
||||||
discard gengine.addEdge(g, src, dst)
|
discard gengine.addEdge(g, src, dst)
|
||||||
let edgeTime = elapsed(edgeStart)
|
let edgeTime = elapsed(edgeStart)
|
||||||
echo " Add ", edgeCount, " edges: ", edgeTime.formatFloat(ffDecimal, 6), "s"
|
let edgeLabel = "Graph-AddEdges"
|
||||||
|
recordResult(edgeLabel, edgeCount, edgeTime)
|
||||||
|
echo " Add ", edgeCount, " edges: ", edgeTime.formatFloat(ffDecimal, 6), "s", compareResult(edgeLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# BFS benchmark
|
# BFS benchmark
|
||||||
let bfsStart = getMonoTime()
|
let bfsStart = getMonoTime()
|
||||||
for i in 0..<100:
|
for i in 0..<100:
|
||||||
discard gengine.bfs(g, NodeId(1))
|
discard gengine.bfs(g, NodeId(1))
|
||||||
let bfsTime = elapsed(bfsStart)
|
let bfsTime = elapsed(bfsStart)
|
||||||
echo " BFS 100 traversals: ", bfsTime.formatFloat(ffDecimal, 3), "s (", formatOps(100, bfsTime), ")"
|
let bfsLabel = "Graph-BFS"
|
||||||
|
recordResult(bfsLabel, 100, bfsTime)
|
||||||
|
echo " BFS 100 traversals: ", bfsTime.formatFloat(ffDecimal, 3), "s (", formatOps(100, bfsTime), ")", compareResult(bfsLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
# PageRank benchmark
|
# PageRank benchmark
|
||||||
let prStart = getMonoTime()
|
let prStart = getMonoTime()
|
||||||
discard gengine.pageRank(g, 10)
|
discard gengine.pageRank(g, 10)
|
||||||
let prTime = elapsed(prStart)
|
let prTime = elapsed(prStart)
|
||||||
echo " PageRank (10 iterations): ", prTime.formatFloat(ffDecimal, 3), "s"
|
let prLabel = "Graph-PageRank"
|
||||||
|
recordResult(prLabel, 10, prTime)
|
||||||
|
echo " PageRank (10 iterations): ", prTime.formatFloat(ffDecimal, 3), "s", compareResult(prLabel, currentResults[^1].opsPerSec, previousResults)
|
||||||
|
|
||||||
proc main() =
|
proc main() =
|
||||||
echo ""
|
echo ""
|
||||||
@@ -226,5 +348,15 @@ proc main() =
|
|||||||
benchGraph()
|
benchGraph()
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Save results for regression tracking
|
||||||
|
let report = BenchReport(
|
||||||
|
version: "1.0.0",
|
||||||
|
gitSha: gitSha(),
|
||||||
|
results: currentResults,
|
||||||
|
)
|
||||||
|
saveResults(ResultsFile, report)
|
||||||
|
echo "Results saved to ", ResultsFile
|
||||||
|
echo ""
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
main()
|
main()
|
||||||
|
|||||||
+142
-1
@@ -1,4 +1,4 @@
|
|||||||
## Property-Based Tests — evalExprValue invariants
|
## Property-Based Tests — evalExprValue + B-Tree invariants
|
||||||
import std/unittest
|
import std/unittest
|
||||||
import std/tables
|
import std/tables
|
||||||
import std/random
|
import std/random
|
||||||
@@ -8,6 +8,7 @@ import std/math
|
|||||||
|
|
||||||
import barabadb/core/types
|
import barabadb/core/types
|
||||||
import barabadb/storage/lsm
|
import barabadb/storage/lsm
|
||||||
|
import barabadb/storage/btree
|
||||||
import barabadb/query/ir as qir
|
import barabadb/query/ir as qir
|
||||||
import barabadb/query/executor as qexec
|
import barabadb/query/executor as qexec
|
||||||
|
|
||||||
@@ -677,3 +678,143 @@ suite "Property-Based — evalExprValue Invariants":
|
|||||||
check v.kind == vkNull
|
check v.kind == vkNull
|
||||||
let s = evalExpr(nil, initTable[string, string](), nil)
|
let s = evalExpr(nil, initTable[string, string](), nil)
|
||||||
check s == ""
|
check s == ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════
|
||||||
|
# B-Tree Property-Based Invariants
|
||||||
|
# ═══════════════════════════════════════════════════
|
||||||
|
suite "Property-Based — B-Tree Invariants":
|
||||||
|
|
||||||
|
proc randKey(rng: var Rand, minVal: int = 0, maxVal: int = 10000): int =
|
||||||
|
rng.rand(minVal..maxVal)
|
||||||
|
|
||||||
|
test "B-Tree size equals number of unique keys after random inserts":
|
||||||
|
var rng = initRand(1000)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 8)
|
||||||
|
var uniqueKeys = initTable[int, bool]()
|
||||||
|
for i in 0..<500:
|
||||||
|
let k = randKey(rng)
|
||||||
|
btree.insert(k, "v" & $k)
|
||||||
|
uniqueKeys[k] = true
|
||||||
|
check btree.len == uniqueKeys.len
|
||||||
|
|
||||||
|
test "B-Tree get returns all values for inserted key":
|
||||||
|
var rng = initRand(1001)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 8)
|
||||||
|
var expected = initTable[int, seq[string]]()
|
||||||
|
for i in 0..<200:
|
||||||
|
let k = randKey(rng, 0, 50)
|
||||||
|
let v = "val_" & $i
|
||||||
|
btree.insert(k, v)
|
||||||
|
if k notin expected:
|
||||||
|
expected[k] = @[]
|
||||||
|
expected[k].add(v)
|
||||||
|
for k, vals in expected:
|
||||||
|
let got = btree.get(k)
|
||||||
|
check got == vals
|
||||||
|
|
||||||
|
test "B-Tree scan returns keys in ascending order":
|
||||||
|
var rng = initRand(1002)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 8)
|
||||||
|
for i in 0..<300:
|
||||||
|
btree.insert(randKey(rng, 0, 1000), "x")
|
||||||
|
let result = btree.scan(0, 1000)
|
||||||
|
for i in 1..<result.len:
|
||||||
|
check result[i-1][0] <= result[i][0]
|
||||||
|
|
||||||
|
test "B-Tree scan range is inclusive and correct":
|
||||||
|
var rng = initRand(1003)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 8)
|
||||||
|
var inserted = initTable[int, bool]()
|
||||||
|
for i in 0..<400:
|
||||||
|
let k = randKey(rng, 0, 200)
|
||||||
|
btree.insert(k, "v")
|
||||||
|
inserted[k] = true
|
||||||
|
let scanned = btree.scan(50, 100)
|
||||||
|
for (k, _) in scanned:
|
||||||
|
check k >= 50
|
||||||
|
check k <= 100
|
||||||
|
check inserted[k]
|
||||||
|
|
||||||
|
test "B-Tree contains after insert":
|
||||||
|
var rng = initRand(1004)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 8)
|
||||||
|
var keys: seq[int] = @[]
|
||||||
|
for i in 0..<100:
|
||||||
|
let k = randKey(rng)
|
||||||
|
btree.insert(k, "v")
|
||||||
|
keys.add(k)
|
||||||
|
for k in keys:
|
||||||
|
check btree.contains(k)
|
||||||
|
|
||||||
|
test "B-Tree remove decreases size":
|
||||||
|
var rng = initRand(1005)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 8)
|
||||||
|
var inserted = initTable[int, seq[string]]()
|
||||||
|
for i in 0..<200:
|
||||||
|
let k = randKey(rng, 0, 100)
|
||||||
|
let v = "v" & $i
|
||||||
|
btree.insert(k, v)
|
||||||
|
if k notin inserted:
|
||||||
|
inserted[k] = @[]
|
||||||
|
inserted[k].add(v)
|
||||||
|
let beforeSize = btree.len
|
||||||
|
var removedCount = 0
|
||||||
|
for k, vals in inserted:
|
||||||
|
if vals.len > 0:
|
||||||
|
btree.remove(k, vals[0])
|
||||||
|
inc removedCount
|
||||||
|
# Size should decrease by number of keys that had values removed
|
||||||
|
# (if all values removed, key is deleted)
|
||||||
|
check btree.len <= beforeSize
|
||||||
|
|
||||||
|
test "B-Tree with large order handles many inserts":
|
||||||
|
var rng = initRand(1006)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 64)
|
||||||
|
for i in 0..<2000:
|
||||||
|
btree.insert(i, "v" & $i)
|
||||||
|
check btree.len == 2000
|
||||||
|
for i in 0..<2000:
|
||||||
|
check btree.contains(i)
|
||||||
|
|
||||||
|
test "B-Tree duplicate inserts append values":
|
||||||
|
var rng = initRand(1007)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 8)
|
||||||
|
let k = 42
|
||||||
|
for i in 0..<50:
|
||||||
|
btree.insert(k, "v" & $i)
|
||||||
|
let vals = btree.get(k)
|
||||||
|
check vals.len == 50
|
||||||
|
for i in 0..<50:
|
||||||
|
check vals[i] == "v" & $i
|
||||||
|
|
||||||
|
test "B-Tree scan on empty tree returns empty":
|
||||||
|
var btree = newBTreeIndex[int, string]()
|
||||||
|
let result = btree.scan(0, 100)
|
||||||
|
check result.len == 0
|
||||||
|
|
||||||
|
test "B-Tree random interleaved insert/remove maintains invariants":
|
||||||
|
var rng = initRand(1008)
|
||||||
|
var btree = newBTreeIndex[int, string](order = 8)
|
||||||
|
var tracker = initTable[int, seq[string]]()
|
||||||
|
for i in 0..<300:
|
||||||
|
let op = rng.rand(0..2)
|
||||||
|
let k = randKey(rng, 0, 50)
|
||||||
|
case op
|
||||||
|
of 0, 1: # insert
|
||||||
|
let v = "v" & $i
|
||||||
|
btree.insert(k, v)
|
||||||
|
if k notin tracker:
|
||||||
|
tracker[k] = @[]
|
||||||
|
tracker[k].add(v)
|
||||||
|
of 2: # remove
|
||||||
|
if k in tracker and tracker[k].len > 0:
|
||||||
|
let v = tracker[k][0]
|
||||||
|
btree.remove(k, v)
|
||||||
|
tracker[k].del(0)
|
||||||
|
if tracker[k].len == 0:
|
||||||
|
tracker.del(k)
|
||||||
|
else: discard
|
||||||
|
# Verify all tracked keys are present
|
||||||
|
for k, vals in tracker:
|
||||||
|
let got = btree.get(k)
|
||||||
|
check got == vals
|
||||||
|
|||||||
Reference in New Issue
Block a user