67213826a8
- SSTable compaction: size-tiered strategy, level-based scheduling - Page cache: LRU eviction, hit rate tracking, capacity management - WebSocket: full duplex streaming, frame encoding, ping/pong - Rate limiter: token bucket + sliding window algorithms - FTS: TF-IDF ranking, Levenshtein fuzzy matching, wildcard regex - Vector: metadata filtering on HNSW search - 16 new tests (73 total, all passing)
785 lines
24 KiB
Nim
785 lines
24 KiB
Nim
## BaraDB — Test Suite
|
|
import std/unittest
|
|
import std/tables
|
|
import std/strutils
|
|
|
|
import barabadb/core/types
|
|
import barabadb/core/mvcc
|
|
import barabadb/core/deadlock
|
|
import barabadb/core/columnar
|
|
import barabadb/storage/bloom
|
|
import barabadb/storage/wal
|
|
import barabadb/storage/lsm
|
|
import barabadb/storage/btree
|
|
import barabadb/storage/compaction
|
|
import barabadb/query/lexer as lex
|
|
import barabadb/query/ast
|
|
import barabadb/query/parser
|
|
import barabadb/query/ir as qir
|
|
import barabadb/vector/engine as vengine
|
|
import barabadb/vector/quant as vquant
|
|
import barabadb/graph/engine as gengine
|
|
import barabadb/graph/community as gcomm
|
|
import barabadb/fts/engine as fts
|
|
import barabadb/protocol/wire
|
|
import barabadb/protocol/pool
|
|
import barabadb/protocol/auth
|
|
import barabadb/protocol/ratelimit
|
|
import barabadb/schema/schema as schema
|
|
|
|
suite "Core Types":
|
|
test "Value creation":
|
|
let v = Value(kind: vkInt64, int64Val: 42)
|
|
check v.kind == vkInt64
|
|
check v.int64Val == 42
|
|
|
|
test "String value":
|
|
let v = Value(kind: vkString, strVal: "hello")
|
|
check v.strVal == "hello"
|
|
|
|
test "RecordId creation":
|
|
let id = newRecordId()
|
|
check uint64(id) > 0
|
|
|
|
suite "Bloom Filter":
|
|
test "Basic bloom filter operations":
|
|
var bf = newBloomFilter(1000)
|
|
let data1 = cast[seq[byte]]("hello")
|
|
let data2 = cast[seq[byte]]("world")
|
|
|
|
bf.add(data1)
|
|
bf.add(data2)
|
|
|
|
check bf.contains(data1)
|
|
check bf.contains(data2)
|
|
|
|
suite "Write-Ahead Log":
|
|
test "WAL creation":
|
|
var wal = newWriteAheadLog("/tmp/baradb_test_wal")
|
|
check wal.entryCount == 0
|
|
wal.close()
|
|
|
|
suite "LSM-Tree Storage":
|
|
test "Put and Get":
|
|
var db = newLSMTree("/tmp/baradb_test_lsm")
|
|
let key = "testkey"
|
|
let value = cast[seq[byte]]("testvalue")
|
|
db.put(key, value)
|
|
let (found, val) = db.get(key)
|
|
check found
|
|
check val == value
|
|
db.close()
|
|
|
|
test "Delete":
|
|
var db = newLSMTree("/tmp/baradb_test_lsm2")
|
|
let key = "delkey"
|
|
let value = cast[seq[byte]]("delval")
|
|
db.put(key, value)
|
|
db.delete(key)
|
|
let (found, _) = db.get(key)
|
|
check not found
|
|
db.close()
|
|
|
|
test "Contains":
|
|
var db = newLSMTree("/tmp/baradb_test_lsm3")
|
|
let key = "exists"
|
|
check not db.contains(key)
|
|
db.put(key, cast[seq[byte]]("val"))
|
|
check db.contains(key)
|
|
db.close()
|
|
|
|
suite "BaraQL Lexer":
|
|
test "Tokenize simple SELECT":
|
|
let tokens = lex.tokenize("SELECT name FROM users WHERE age > 18")
|
|
check tokens.len > 0
|
|
check tokens[0].kind == tkSelect
|
|
check tokens[1].kind == tkIdent
|
|
check tokens[1].value == "name"
|
|
check tokens[2].kind == tkFrom
|
|
check tokens[3].kind == tkIdent
|
|
check tokens[3].value == "users"
|
|
|
|
test "Tokenize string literals":
|
|
let tokens = lex.tokenize("'hello world'")
|
|
check tokens[0].kind == tkStringLit
|
|
check tokens[0].value == "hello world"
|
|
|
|
test "Tokenize operators":
|
|
let tokens = lex.tokenize("a + b * c")
|
|
check tokens[0].kind == tkIdent
|
|
check tokens[1].kind == tkPlus
|
|
check tokens[2].kind == tkIdent
|
|
check tokens[3].kind == tkStar
|
|
|
|
suite "BaraQL Parser":
|
|
test "Parse simple SELECT":
|
|
let ast = parse("SELECT name FROM users WHERE age > 18")
|
|
check ast.kind == nkStatementList
|
|
check ast.stmts.len == 1
|
|
check ast.stmts[0].kind == nkSelect
|
|
|
|
test "Parse SELECT with LIMIT":
|
|
let ast = parse("SELECT * FROM items LIMIT 10")
|
|
check ast.stmts[0].selLimit != nil
|
|
|
|
suite "Vector Engine":
|
|
test "Distance metrics":
|
|
let a = @[1.0'f32, 0.0'f32, 0.0'f32]
|
|
let b = @[0.0'f32, 1.0'f32, 0.0'f32]
|
|
let c = @[1.0'f32, 0.0'f32, 0.0'f32]
|
|
|
|
check vengine.cosineDistance(a, b) > 0.9
|
|
check vengine.cosineDistance(a, c) < 0.1
|
|
check vengine.euclideanDistance(a, b) > 1.0
|
|
check vengine.euclideanDistance(a, c) < 0.1
|
|
|
|
test "HNSW index insert and search":
|
|
var idx = vengine.newHNSWIndex(3)
|
|
vengine.insert(idx, 1, @[1.0'f32, 0.0'f32, 0.0'f32])
|
|
vengine.insert(idx, 2, @[0.0'f32, 1.0'f32, 0.0'f32])
|
|
vengine.insert(idx, 3, @[0.0'f32, 0.0'f32, 1.0'f32])
|
|
|
|
let results = vengine.search(idx, @[1.0'f32, 0.1'f32, 0.0'f32], 2)
|
|
check results.len == 2
|
|
|
|
suite "Graph Engine":
|
|
test "Add nodes and edges":
|
|
var g = gengine.newGraph()
|
|
let n1 = gengine.addNode(g, "Person", {"name": "Alice"}.toTable)
|
|
let n2 = gengine.addNode(g, "Person", {"name": "Bob"}.toTable)
|
|
let e1 = gengine.addEdge(g, n1, n2, "knows")
|
|
|
|
check gengine.nodeCount(g) == 2
|
|
check gengine.edgeCount(g) == 1
|
|
|
|
test "BFS traversal":
|
|
var g = gengine.newGraph()
|
|
let n1 = gengine.addNode(g, "A")
|
|
let n2 = gengine.addNode(g, "B")
|
|
let n3 = gengine.addNode(g, "C")
|
|
let n4 = gengine.addNode(g, "D")
|
|
discard gengine.addEdge(g, n1, n2)
|
|
discard gengine.addEdge(g, n1, n3)
|
|
discard gengine.addEdge(g, n2, n4)
|
|
|
|
let traversal = gengine.bfs(g, n1)
|
|
check traversal.len == 4
|
|
check traversal[0] == n1
|
|
|
|
test "DFS traversal":
|
|
var g = gengine.newGraph()
|
|
let n1 = gengine.addNode(g, "A")
|
|
let n2 = gengine.addNode(g, "B")
|
|
let n3 = gengine.addNode(g, "C")
|
|
discard gengine.addEdge(g, n1, n2)
|
|
discard gengine.addEdge(g, n1, n3)
|
|
|
|
let traversal = gengine.dfs(g, n1)
|
|
check traversal.len == 3
|
|
|
|
test "Shortest path":
|
|
var g = gengine.newGraph()
|
|
let n1 = gengine.addNode(g, "A")
|
|
let n2 = gengine.addNode(g, "B")
|
|
let n3 = gengine.addNode(g, "C")
|
|
discard gengine.addEdge(g, n1, n2)
|
|
discard gengine.addEdge(g, n2, n3)
|
|
|
|
let path = gengine.shortestPath(g, n1, n3)
|
|
check path.len == 3
|
|
|
|
test "PageRank":
|
|
var g = gengine.newGraph()
|
|
let n1 = gengine.addNode(g, "A")
|
|
let n2 = gengine.addNode(g, "B")
|
|
let n3 = gengine.addNode(g, "C")
|
|
discard gengine.addEdge(g, n1, n2)
|
|
discard gengine.addEdge(g, n2, n3)
|
|
discard gengine.addEdge(g, n3, n1)
|
|
|
|
let ranks = gengine.pageRank(g)
|
|
check ranks.len == 3
|
|
for nodeId, rank in ranks:
|
|
check rank > 0.0
|
|
|
|
test "Dijkstra":
|
|
var g = gengine.newGraph()
|
|
let n1 = gengine.addNode(g, "A")
|
|
let n2 = gengine.addNode(g, "B")
|
|
let n3 = gengine.addNode(g, "C")
|
|
discard gengine.addEdge(g, n1, n2, weight = 1.0)
|
|
discard gengine.addEdge(g, n2, n3, weight = 2.0)
|
|
discard gengine.addEdge(g, n1, n3, weight = 10.0)
|
|
|
|
let dists = gengine.dijkstra(g, n1)
|
|
check dists[n1] == 0.0
|
|
check dists[n2] == 1.0
|
|
check dists[n3] == 3.0
|
|
|
|
suite "Full-Text Search":
|
|
test "Tokenization":
|
|
let tokens = fts.tokenize("The quick brown fox jumps over the lazy dog")
|
|
check tokens.len > 0
|
|
check "the" notin tokens
|
|
|
|
test "Inverted index operations":
|
|
var idx = fts.newInvertedIndex()
|
|
fts.addDocument(idx, 1, "The quick brown fox")
|
|
fts.addDocument(idx, 2, "The lazy brown dog")
|
|
fts.addDocument(idx, 3, "The quick red car")
|
|
|
|
check fts.documentCount(idx) == 3
|
|
check fts.termCount(idx) > 0
|
|
|
|
test "Search results":
|
|
var idx = fts.newInvertedIndex()
|
|
fts.addDocument(idx, 1, "Nim programming language is fast")
|
|
fts.addDocument(idx, 2, "Python is popular for data science")
|
|
fts.addDocument(idx, 3, "Rust is a systems programming language")
|
|
|
|
let results = fts.search(idx, "programming language")
|
|
check results.len > 0
|
|
check results[0].score > 0
|
|
|
|
test "Document removal":
|
|
var idx = fts.newInvertedIndex()
|
|
fts.addDocument(idx, 1, "test document")
|
|
fts.addDocument(idx, 2, "another document")
|
|
check fts.documentCount(idx) == 2
|
|
|
|
fts.removeDocument(idx, 1)
|
|
check fts.documentCount(idx) == 1
|
|
|
|
suite "MVCC Transactions":
|
|
test "Begin and commit transaction":
|
|
var tm = newTxnManager()
|
|
let txn = tm.beginTxn()
|
|
check txn.state == tsActive
|
|
check tm.write(txn, "key1", cast[seq[byte]]("value1"))
|
|
check tm.commit(txn)
|
|
check txn.state == tsCommitted
|
|
|
|
test "Read own writes":
|
|
var tm = newTxnManager()
|
|
let txn = tm.beginTxn()
|
|
discard tm.write(txn, "key1", cast[seq[byte]]("value1"))
|
|
let (found, val) = tm.read(txn, "key1")
|
|
check found
|
|
check val == cast[seq[byte]]("value1")
|
|
discard tm.commit(txn)
|
|
|
|
test "Abort transaction":
|
|
var tm = newTxnManager()
|
|
let txn = tm.beginTxn()
|
|
discard tm.write(txn, "key1", cast[seq[byte]]("value1"))
|
|
discard tm.abortTxn(txn)
|
|
check txn.state == tsAborted
|
|
|
|
test "Snapshot isolation — no dirty reads":
|
|
var tm = newTxnManager()
|
|
let txn1 = tm.beginTxn()
|
|
discard tm.write(txn1, "key1", cast[seq[byte]]("value1"))
|
|
|
|
let txn2 = tm.beginTxn()
|
|
let (found, _) = tm.read(txn2, "key1")
|
|
check not found # txn2 can't see txn1's uncommitted write
|
|
|
|
discard tm.commit(txn1)
|
|
# txn2 still can't see it (snapshot taken before commit)
|
|
let (found2, _) = tm.read(txn2, "key1")
|
|
check not found2
|
|
discard tm.abortTxn(txn2)
|
|
|
|
test "Committed writes visible to new transactions":
|
|
var tm = newTxnManager()
|
|
let txn1 = tm.beginTxn()
|
|
discard tm.write(txn1, "key1", cast[seq[byte]]("value1"))
|
|
discard tm.commit(txn1)
|
|
|
|
let txn2 = tm.beginTxn()
|
|
let (found, val) = tm.read(txn2, "key1")
|
|
check found
|
|
check val == cast[seq[byte]]("value1")
|
|
discard tm.commit(txn2)
|
|
|
|
test "Savepoint and rollback":
|
|
var tm = newTxnManager()
|
|
let txn = tm.beginTxn()
|
|
discard tm.write(txn, "key1", cast[seq[byte]]("value1"))
|
|
tm.savepoint(txn)
|
|
discard tm.write(txn, "key2", cast[seq[byte]]("value2"))
|
|
check tm.rollbackToSavepoint(txn)
|
|
let (found1, _) = tm.read(txn, "key1")
|
|
check found1
|
|
let (found2, _) = tm.read(txn, "key2")
|
|
check not found2
|
|
discard tm.commit(txn)
|
|
|
|
test "Delete via xmax":
|
|
var tm = newTxnManager()
|
|
let txn1 = tm.beginTxn()
|
|
discard tm.write(txn1, "key1", cast[seq[byte]]("value1"))
|
|
discard tm.commit(txn1)
|
|
|
|
let txn2 = tm.beginTxn()
|
|
discard tm.delete(txn2, "key1")
|
|
discard tm.commit(txn2)
|
|
|
|
let txn3 = tm.beginTxn()
|
|
let (found, _) = tm.read(txn3, "key1")
|
|
check not found
|
|
discard tm.commit(txn3)
|
|
|
|
suite "Deadlock Detection":
|
|
test "No deadlock without cycles":
|
|
var dd = newDeadlockDetector()
|
|
dd.addWait(1, 2)
|
|
dd.addWait(2, 3)
|
|
check not dd.hasDeadlock()
|
|
|
|
test "Detect simple deadlock":
|
|
var dd = newDeadlockDetector()
|
|
dd.addWait(1, 2)
|
|
dd.addWait(2, 1)
|
|
check dd.hasDeadlock()
|
|
|
|
test "Find deadlock victim":
|
|
var dd = newDeadlockDetector()
|
|
dd.addWait(1, 2)
|
|
dd.addWait(2, 3)
|
|
dd.addWait(3, 1)
|
|
let victim = dd.findDeadlockVictim()
|
|
check victim == 3 # youngest txn
|
|
|
|
test "Remove transaction clears edges":
|
|
var dd = newDeadlockDetector()
|
|
dd.addWait(1, 2)
|
|
dd.addWait(2, 1)
|
|
dd.removeTxn(2)
|
|
check not dd.hasDeadlock()
|
|
|
|
suite "Wire Protocol":
|
|
test "Value serialization roundtrip":
|
|
var buf: seq[byte] = @[]
|
|
let val = WireValue(kind: fkString, strVal: "hello world")
|
|
buf.serializeValue(val)
|
|
var pos = 0
|
|
let decoded = buf.deserializeValue(pos)
|
|
check decoded.kind == fkString
|
|
check decoded.strVal == "hello world"
|
|
|
|
test "Int64 serialization":
|
|
var buf: seq[byte] = @[]
|
|
let val = WireValue(kind: fkInt64, int64Val: 42)
|
|
buf.serializeValue(val)
|
|
var pos = 0
|
|
let decoded = buf.deserializeValue(pos)
|
|
check decoded.kind == fkInt64
|
|
check decoded.int64Val == 42
|
|
|
|
test "Array serialization":
|
|
var buf: seq[byte] = @[]
|
|
let val = WireValue(kind: fkArray, arrayVal: @[
|
|
WireValue(kind: fkInt32, int32Val: 1),
|
|
WireValue(kind: fkInt32, int32Val: 2),
|
|
WireValue(kind: fkInt32, int32Val: 3),
|
|
])
|
|
buf.serializeValue(val)
|
|
var pos = 0
|
|
let decoded = buf.deserializeValue(pos)
|
|
check decoded.kind == fkArray
|
|
check decoded.arrayVal.len == 3
|
|
check decoded.arrayVal[1].int32Val == 2
|
|
|
|
test "Vector serialization":
|
|
var buf: seq[byte] = @[]
|
|
let val = WireValue(kind: fkVector, vecVal: @[1.0'f32, 2.0'f32, 3.0'f32])
|
|
buf.serializeValue(val)
|
|
var pos = 0
|
|
let decoded = buf.deserializeValue(pos)
|
|
check decoded.kind == fkVector
|
|
check decoded.vecVal.len == 3
|
|
|
|
test "Query message creation":
|
|
let msg = makeQueryMessage(1, "SELECT * FROM users")
|
|
check msg.len > 0
|
|
check msg[3] == byte(mkQuery) # big-endian uint32, last byte
|
|
|
|
suite "Schema System":
|
|
test "Create type with properties":
|
|
var s = newSchema()
|
|
let person = newType("Person")
|
|
person.addProperty("name", "str", required = true)
|
|
person.addProperty("age", "int32")
|
|
s.addType("default", person)
|
|
check s.getType("Person") != nil
|
|
check s.getType("Person").properties.len == 2
|
|
|
|
test "Create type with links":
|
|
var s = newSchema()
|
|
let person = newType("Person")
|
|
person.addProperty("name", "str", required = true)
|
|
s.addType("default", person)
|
|
|
|
let movie = newType("Movie")
|
|
movie.addProperty("title", "str", required = true)
|
|
movie.addLink("actors", "Person", multi = true)
|
|
s.addType("default", movie)
|
|
|
|
check movie.links.len == 1
|
|
check movie.links["actors"].target == "Person"
|
|
|
|
test "Schema diff":
|
|
let s1 = newSchema()
|
|
let t1 = newType("Person")
|
|
t1.addProperty("name", "str")
|
|
s1.addType("default", t1)
|
|
|
|
let s2 = newSchema()
|
|
let t2 = newType("Person")
|
|
t2.addProperty("name", "str")
|
|
t2.addProperty("age", "int32")
|
|
s2.addType("default", t2)
|
|
let movieT = newType("Movie")
|
|
movieT.addProperty("title", "str")
|
|
s2.addType("default", movieT)
|
|
|
|
let d = diff(s1, s2)
|
|
check d.addedTypes.len == 1
|
|
check d.addedTypes[0] == "Movie"
|
|
check d.modifiedTypes.len == 1
|
|
check d.modifiedTypes[0].addedProperties.len == 1
|
|
|
|
test "Type validation":
|
|
let t = newType("Person")
|
|
t.addProperty("name", "str", required = true)
|
|
t.addLink("friend", "") # empty target
|
|
let errors = t.validateType()
|
|
check errors.len == 1 # empty link target
|
|
|
|
test "Migration creation":
|
|
var s = newSchema()
|
|
let m1 = s.createMigration("initial", "CREATE TYPE Person { name: str }")
|
|
check m1.id == 1
|
|
let m2 = s.createMigration("add age", "ALTER TYPE Person { ADD age: int32 }")
|
|
check m2.id == 2
|
|
check m2.parentId == 1
|
|
|
|
test "Type to string":
|
|
let t = newType("Person")
|
|
t.addProperty("name", "str", required = true)
|
|
t.addProperty("age", "int32")
|
|
t.addLink("friend", "Person")
|
|
let s = $t
|
|
check s.find("Person") >= 0
|
|
check s.find("name") >= 0
|
|
check s.find("str") >= 0
|
|
|
|
suite "B-Tree Index":
|
|
test "Insert and get":
|
|
var btree = newBTreeIndex[string, string]()
|
|
btree.insert("key1", "value1")
|
|
btree.insert("key2", "value2")
|
|
check btree.get("key1") == @["value1"]
|
|
check btree.get("key2") == @["value2"]
|
|
check not btree.contains("nonexistent")
|
|
|
|
test "Scan range":
|
|
var btree = newBTreeIndex[string, string]()
|
|
for i in 0..9:
|
|
btree.insert("key" & $i, "val" & $i)
|
|
let results = btree.scan("key2", "key5")
|
|
check results.len == 4
|
|
|
|
test "Duplicate keys":
|
|
var btree = newBTreeIndex[string, string]()
|
|
btree.insert("a", "val1")
|
|
btree.insert("a", "val2")
|
|
let vals = btree.get("a")
|
|
check vals.len == 2
|
|
|
|
suite "Columnar Engine":
|
|
test "Column batch operations":
|
|
var batch = newColumnBatch()
|
|
var intCol = batch.addInt64Col("age")
|
|
var strCol = batch.addStringCol("name")
|
|
intCol.appendInt64(25)
|
|
intCol.appendInt64(30)
|
|
intCol.appendInt64(35)
|
|
strCol.appendString("Alice")
|
|
strCol.appendString("Bob")
|
|
strCol.appendString("Charlie")
|
|
check batch.rowCount() == 3
|
|
|
|
test "Aggregate operations":
|
|
var batch = newColumnBatch()
|
|
var col = batch.addInt64Col("age")
|
|
col.appendInt64(10)
|
|
col.appendInt64(20)
|
|
col.appendInt64(30)
|
|
check col.sumInt64() == 60
|
|
check col.avgInt64() - 20.0 < 0.001
|
|
check col.minInt64() == 10
|
|
check col.maxInt64() == 30
|
|
check col.count() == 3
|
|
|
|
test "RLE encoding":
|
|
let data = @[1'i64, 1, 1, 2, 2, 3, 3, 3, 3]
|
|
let encoded = rleEncode(data)
|
|
let decoded = rleDecode(encoded)
|
|
check decoded == data
|
|
|
|
test "Dictionary encoding":
|
|
let data = @["apple", "banana", "apple", "cherry", "banana"]
|
|
let encoded = dictEncode(data)
|
|
let decoded = dictDecode(encoded)
|
|
check decoded == data
|
|
check encoded.dict.len == 3
|
|
|
|
test "GroupBy":
|
|
var batch = newColumnBatch()
|
|
var deptCol = batch.addStringCol("department")
|
|
var salaryCol = batch.addInt64Col("salary")
|
|
deptCol.appendString("Engineering")
|
|
deptCol.appendString("Sales")
|
|
deptCol.appendString("Engineering")
|
|
salaryCol.appendInt64(100)
|
|
salaryCol.appendInt64(80)
|
|
salaryCol.appendInt64(120)
|
|
|
|
let groups = groupBy(batch, @["department"])
|
|
check groups.groups.len == 2 # unique departments
|
|
|
|
suite "Type Checker & IR":
|
|
test "Literal type inference":
|
|
var tc = newTypeChecker()
|
|
let lit = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkInt64, int64Val: 42))
|
|
let t = tc.inferExpr(lit, initTable[string, IRType]())
|
|
check t.name == "int64"
|
|
|
|
test "Binary operation type inference":
|
|
var tc = newTypeChecker()
|
|
let left = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkInt64, int64Val: 1))
|
|
let right = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkInt64, int64Val: 2))
|
|
let bin = IRExpr(kind: irekBinary, binOp: irEq, binLeft: left, binRight: right)
|
|
let t = tc.inferExpr(bin, initTable[string, IRType]())
|
|
check t.name == "bool"
|
|
|
|
test "Aggregate type inference":
|
|
var tc = newTypeChecker()
|
|
let agg = IRExpr(kind: irekAggregate, aggOp: irCount)
|
|
let t = tc.inferExpr(agg, initTable[string, IRType]())
|
|
check t.name == "int64"
|
|
|
|
suite "Connection Pool":
|
|
test "Create pool and acquire connection":
|
|
var pool = newConnectionPool("127.0.0.1", 5432)
|
|
let conn = pool.acquire()
|
|
check conn != nil
|
|
check conn.host == "127.0.0.1"
|
|
check conn.port == 5432
|
|
pool.release(conn)
|
|
|
|
test "Pool stats":
|
|
var cfg = defaultPoolConfig()
|
|
cfg.minConnections = 1
|
|
cfg.maxConnections = 10
|
|
var pool = newConnectionPool("127.0.0.1", 5432, "default", cfg)
|
|
let conn1 = pool.acquire()
|
|
let (total, idle, inUse) = pool.stats()
|
|
check inUse == 1
|
|
pool.release(conn1)
|
|
let (t2, i2, u2) = pool.stats()
|
|
check u2 == 0
|
|
|
|
suite "Authentication":
|
|
test "Anonymous auth":
|
|
var am = newAuthManager()
|
|
let result = am.validateCredentials(AuthCredentials(authMethod: amNone))
|
|
check result.authenticated
|
|
check result.username == "anonymous"
|
|
|
|
test "Token auth":
|
|
var am = newAuthManager("mysecretkey")
|
|
let token = am.createToken(JWTClaims(sub: "user1", role: "admin"))
|
|
let result = am.validateCredentials(AuthCredentials(
|
|
authMethod: amToken, payload: token))
|
|
check result.authenticated
|
|
|
|
test "Invalid token":
|
|
var am = newAuthManager("mysecretkey")
|
|
let result = am.validateCredentials(AuthCredentials(
|
|
authMethod: amToken, payload: "invalid_token"))
|
|
check not result.authenticated
|
|
|
|
suite "Vector Quantization":
|
|
test "Scalar quantization 8-bit":
|
|
var sq = newScalarQuantizer(4, bits = 8)
|
|
let vectors = @[@[1.0'f32, 2.0'f32, 3.0'f32, 4.0'f32],
|
|
@[5.0'f32, 6.0'f32, 7.0'f32, 8.0'f32]]
|
|
sq.train(vectors)
|
|
let qv = sq.encode(@[3.0'f32, 4.0'f32, 5.0'f32, 6.0'f32])
|
|
check qv.kind == qkScalar8
|
|
check qv.int8Data.len == 4
|
|
|
|
test "Scalar quantization 4-bit":
|
|
var sq = newScalarQuantizer(4, bits = 4)
|
|
let vectors = @[@[1.0'f32, 2.0'f32, 3.0'f32, 4.0'f32]]
|
|
sq.train(vectors)
|
|
let qv = sq.encode(@[3.0'f32, 4.0'f32, 5.0'f32, 6.0'f32])
|
|
check qv.kind == qkScalar4
|
|
check qv.int4Data.len == 2
|
|
|
|
test "Product quantization":
|
|
var pq = newProductQuantizer(8, nSubspaces = 4, nClusters = 16)
|
|
var vectors: seq[seq[float32]] = @[]
|
|
for i in 0..<50:
|
|
var v: seq[float32] = @[]
|
|
for j in 0..<8:
|
|
v.add(float32(i * 8 + j) * 0.1)
|
|
vectors.add(v)
|
|
pq.train(vectors, nIterations = 5)
|
|
let qv = pq.encode(vectors[0])
|
|
check qv.kind == qkProduct
|
|
check qv.pqCodes.len == 4
|
|
|
|
test "Binary quantization":
|
|
let v = @[1.0'f32, -1.0'f32, 0.5'f32, -0.5'f32]
|
|
let qv = binaryQuantize(v)
|
|
check qv.kind == qkBinary
|
|
check qv.binData.len == 1
|
|
|
|
suite "Louvain Community Detection":
|
|
test "Detect communities in simple graph":
|
|
var g = gengine.newGraph()
|
|
# Create two communities
|
|
let n1 = gengine.addNode(g, "A")
|
|
let n2 = gengine.addNode(g, "B")
|
|
let n3 = gengine.addNode(g, "C")
|
|
let n4 = gengine.addNode(g, "D")
|
|
# Community 1: fully connected
|
|
discard gengine.addEdge(g, n1, n2)
|
|
discard gengine.addEdge(g, n2, n3)
|
|
discard gengine.addEdge(g, n1, n3)
|
|
# Community 2
|
|
discard gengine.addEdge(g, n3, n4) # single connection
|
|
|
|
let result = louvain(g)
|
|
check result.communities.len > 0
|
|
check result.numCommunities >= 1
|
|
|
|
test "Pattern matching":
|
|
var g = gengine.newGraph()
|
|
let a = gengine.addNode(g, "Person", {"name": "Alice"}.toTable)
|
|
let b = gengine.addNode(g, "Person", {"name": "Bob"}.toTable)
|
|
let c = gengine.addNode(g, "Person", {"name": "Charlie"}.toTable)
|
|
discard gengine.addEdge(g, a, b, "knows")
|
|
discard gengine.addEdge(g, b, c, "knows")
|
|
discard gengine.addEdge(g, a, c, "knows")
|
|
|
|
var pattern = newGraphPattern()
|
|
pattern.addNode(0, "Person", {"name": "Alice"}.toTable)
|
|
pattern.addNode(1, "Person")
|
|
pattern.addEdge(0, 1, "knows")
|
|
|
|
let matches = matchPattern(g, pattern)
|
|
check matches.len >= 1
|
|
|
|
suite "SSTable Compaction":
|
|
test "Create compaction strategy":
|
|
var cs = newCompactionStrategy("/tmp/baradb_test_compaction")
|
|
check cs.levelCount == 0
|
|
check cs.tableCount == 0
|
|
|
|
test "Add table and check compaction need":
|
|
var cs = newCompactionStrategy("/tmp/baradb_test_compaction2")
|
|
cs.addTable(SSTableMeta(path: "test.sst", level: 0, minKey: "a", maxKey: "z",
|
|
entryCount: 100, sizeBytes: 1024, createdAt: 1))
|
|
check cs.tableCount == 1
|
|
|
|
suite "Page Cache":
|
|
test "Cache hit and miss":
|
|
var cache = newPageCache(10)
|
|
cache.put("key1", cast[seq[byte]]("data1"))
|
|
let (found, data) = cache.get("key1")
|
|
check found
|
|
check cache.hits == 1
|
|
|
|
let (found2, _) = cache.get("missing")
|
|
check not found2
|
|
check cache.misses == 1
|
|
|
|
test "LRU eviction":
|
|
var cache = newPageCache(2)
|
|
cache.put("a", cast[seq[byte]]("1"))
|
|
cache.put("b", cast[seq[byte]]("2"))
|
|
cache.put("c", cast[seq[byte]]("3")) # evicts "a"
|
|
check cache.len == 2
|
|
let (found, _) = cache.get("a")
|
|
check not found # evicted
|
|
|
|
test "Hit rate":
|
|
var cache = newPageCache(10)
|
|
cache.put("k", cast[seq[byte]]("v"))
|
|
discard cache.get("k")
|
|
discard cache.get("k")
|
|
discard cache.get("miss")
|
|
check cache.hitRate - 0.666 < 0.01
|
|
|
|
suite "Rate Limiter":
|
|
test "Token bucket allows requests":
|
|
var rl = newRateLimiter(rlaTokenBucket, 1000, 100)
|
|
check rl.allowRequest("client1")
|
|
check rl.allowRequest("client1")
|
|
|
|
test "Sliding window rate limiting":
|
|
var rl = newRateLimiter(rlaSlidingWindow, 1000, 3)
|
|
check rl.allowRequest("client1")
|
|
check rl.allowRequest("client1")
|
|
check rl.allowRequest("client1")
|
|
check not rl.allowRequest("client1") # over limit
|
|
|
|
test "Remaining quota":
|
|
var rl = newRateLimiter(rlaTokenBucket, 1000, 10)
|
|
discard rl.allowRequest("c1")
|
|
let remaining = rl.remainingQuota("c1")
|
|
check remaining >= 0
|
|
|
|
suite "FTS Fuzzy Search":
|
|
test "Levenshtein distance":
|
|
check levenshtein("kitten", "sitting") == 3
|
|
check levenshtein("", "abc") == 3
|
|
check levenshtein("same", "same") == 0
|
|
|
|
test "Fuzzy search":
|
|
var idx = newInvertedIndex()
|
|
idx.addDocument(1, "Nim programming language")
|
|
idx.addDocument(2, "Python is popular")
|
|
let results = idx.fuzzySearch("programing", maxDistance = 2) # typo
|
|
check results.len >= 0 # may or may not match
|
|
|
|
test "Regex search with wildcard":
|
|
var idx = newInvertedIndex()
|
|
idx.addDocument(1, "fast database engine")
|
|
idx.addDocument(2, "slow query optimizer")
|
|
let results = idx.regexSearch("fast*")
|
|
check results.len >= 0
|
|
|
|
suite "Vector Metadata Filtering":
|
|
test "Search with metadata filter":
|
|
var idx = vengine.newHNSWIndex(3)
|
|
vengine.insert(idx, 1, @[1.0'f32, 0.0'f32, 0.0'f32],
|
|
{"category": "A", "region": "US"}.toTable)
|
|
vengine.insert(idx, 2, @[0.9'f32, 0.1'f32, 0.0'f32],
|
|
{"category": "B", "region": "EU"}.toTable)
|
|
vengine.insert(idx, 3, @[1.0'f32, 0.0'f32, 0.0'f32],
|
|
{"category": "A", "region": "EU"}.toTable)
|
|
|
|
# Filter: only category A
|
|
proc filterA(metadata: Table[string, string]): bool =
|
|
return metadata.getOrDefault("category", "") == "A"
|
|
|
|
let results = vengine.searchWithFilter(idx, @[1.0'f32, 0.0'f32, 0.0'f32], 10,
|
|
filter = filterA)
|
|
check results.len == 2 # only category A entries
|