diff --git a/tests/test_all.nim b/tests/test_all.nim index e39021b..20e332d 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -55,6 +55,2244 @@ import barabadb/protocol/scram 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": + removeDir("/tmp/baradb_test_lsm") + 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": + removeDir("/tmp/baradb_test_lsm2") + 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": + removeDir("/tmp/baradb_test_lsm3") + 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 + + test "Save and load graph": + var g = gengine.newGraph() + let n1 = gengine.addNode(g, "Person", {"name": "Alice"}.toTable) + let n2 = gengine.addNode(g, "Person", {"name": "Bob"}.toTable) + let n3 = gengine.addNode(g, "City", {"name": "Sofia"}.toTable) + discard gengine.addEdge(g, n1, n2, "knows", {"since": "2020"}.toTable, 1.5) + discard gengine.addEdge(g, n2, n3, "lives_in", weight = 2.0) + + let path = "/tmp/baradb_test_graph.bin" + removeFile(path) + gengine.saveToFile(g, path) + + let g2 = gengine.loadFromFile(path) + check gengine.nodeCount(g2) == 3 + check gengine.edgeCount(g2) == 2 + check gengine.neighbors(g2, n1).len == 1 + check gengine.neighbors(g2, n1)[0] == n2 + check gengine.neighbors(g2, n2)[0] == n3 + + let loadedNode = gengine.getNode(g2, n1) + check loadedNode.label == "Person" + check loadedNode.properties["name"] == "Alice" + + let sp = gengine.shortestPath(g2, n1, n3) + check sp.len == 3 + + removeFile(path) + +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 "MVCC Deadlock Detection": + test "TxnManager detects and breaks deadlock": + var tm = newTxnManager() + var t1 = tm.beginTxn() + var t2 = tm.beginTxn() + # t1 writes key "a" + check tm.write(t1, "a", @[1'u8]) + # t2 writes key "b" + check tm.write(t2, "b", @[2'u8]) + # t2 tries to write "a" — conflicts with t1 (active), adds wait edge t2->t1 + check not tm.write(t2, "a", @[3'u8]) + # t1 tries to write "b" — conflicts with t2 (active), adds wait edge t1->t2 + # This creates a cycle: t1->t2->t1 + check not tm.write(t1, "b", @[4'u8]) + # One of the transactions should have been aborted as victim + let t1Active = t1.state == tsActive + let t2Active = t2.state == tsActive + # At least one victim must be aborted + check (not t1Active) or (not t2Active) + # The survivor should be able to commit + if t1Active: + check tm.commit(t1) + if t2Active: + check tm.commit(t2) + + test "No false deadlock on sequential writes": + var tm = newTxnManager() + var t1 = tm.beginTxn() + # t1 writes key "a" + check tm.write(t1, "a", @[1'u8]) + # t1 commits + check tm.commit(t1) + # t2 begins after t1 committed + var t2 = tm.beginTxn() + # t2 writes same key — no active conflict + check tm.write(t2, "a", @[2'u8]) + check tm.commit(t2) + +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", 9472) + let conn = pool.acquire() + check conn != nil + check conn.host == "127.0.0.1" + check conn.port == 9472 + pool.release(conn) + + test "Pool stats": + var cfg = defaultPoolConfig() + cfg.minConnections = 1 + cfg.maxConnections = 10 + var pool = newConnectionPool("127.0.0.1", 9472, "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 + + test "SCRAM-SHA-256 full handshake": + var am = newAuthManager() + am.registerScramUser("alice", "wonderland") + + # Step 1: ClientFirstMessage + let clientNonce = generateNonce() + let clientFirst = "n,,n=alice,r=" & clientNonce + + # Step 2: ServerFirstMessage + let serverFirst = am.startScram(clientFirst) + check serverFirst.startsWith("r=") + check serverFirst.contains("s=") + check serverFirst.contains("i=4096") + + # Parse serverFirst + var combinedNonce = "" + var saltB64 = "" + var iterCount = 0 + for part in serverFirst.split(","): + if part.startsWith("r="): combinedNonce = part[2..^1] + elif part.startsWith("s="): saltB64 = part[2..^1] + elif part.startsWith("i="): iterCount = parseInt(part[2..^1]) + check combinedNonce.len > 0 + check saltB64.len > 0 + check iterCount == 4096 + + # Step 3: Compute client proof + let salt = decode(saltB64) + let saltedPassword = pbkdf2HmacSha256("wonderland", salt, iterCount) + let clientKey = hmacSha256(saltedPassword, "Client Key") + let storedKey = sha256(clientKey) + let serverKey = hmacSha256(saltedPassword, "Server Key") + + let clientFirstBare = "n=alice,r=" & clientNonce + let clientFinalWithoutProof = "c=biws,r=" & combinedNonce + let authMessage = clientFirstBare & "," & serverFirst & "," & clientFinalWithoutProof + + let clientSignature = hmacSha256(storedKey, authMessage) + var clientProof = newSeq[byte](32) + for i in 0..<32: + clientProof[i] = clientKey[i] xor clientSignature[i] + var proofStr = newString(32) + copyMem(addr proofStr[0], addr clientProof[0], 32) + let proofB64 = encode(proofStr) + # Strip padding + var proofB64Clean = proofB64 + while proofB64Clean.endsWith("="): + proofB64Clean.setLen(proofB64Clean.len - 1) + proofB64Clean = proofB64Clean.replace("+", "-").replace("/", "_") + + let clientFinal = "c=biws,r=" & combinedNonce & ",p=" & proofB64Clean + + # Step 4: Server verifies + let (ok, serverFinal) = am.finishScram(clientFinal) + check ok + check serverFinal.startsWith("v=") + + # Verify server signature ourselves + let expectedServerSig = hmacSha256(serverKey, authMessage) + var expectedStr = newString(32) + copyMem(addr expectedStr[0], unsafeAddr expectedServerSig[0], 32) + let expectedB64 = encode(expectedStr) + check serverFinal == "v=" & expectedB64 + + test "SCRAM-SHA-256 invalid proof rejected": + var am = newAuthManager() + am.registerScramUser("bob", "builder") + + let clientNonce = generateNonce() + let clientFirst = "n,,n=bob,r=" & clientNonce + let serverFirst = am.startScram(clientFirst) + + var combinedNonce = "" + for part in serverFirst.split(","): + if part.startsWith("r="): combinedNonce = part[2..^1] + + let clientFinal = "c=biws,r=" & combinedNonce & ",p=invalidproof" + let (ok, serverFinal) = am.finishScram(clientFinal) + check not ok + check serverFinal == "e=invalid-proof" + +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 + +suite "BaraQL Parser — Extended": + test "Parse GROUP BY": + let ast = parse("SELECT dept, count(*) FROM employees GROUP BY dept") + check ast.stmts.len == 1 + check ast.stmts[0].selGroupBy.len == 1 + + test "Parse GROUP BY with HAVING": + let ast = parse("SELECT dept, count(*) FROM employees GROUP BY dept HAVING count(*) > 5") + check ast.stmts[0].selHaving != nil + + test "Parse ORDER BY with direction": + let ast = parse("SELECT name FROM users ORDER BY age DESC") + check ast.stmts[0].selOrderBy.len == 1 + check ast.stmts[0].selOrderBy[0].orderByDir == sdDesc + + test "Parse ORDER BY multiple columns": + let ast = parse("SELECT * FROM t ORDER BY a ASC, b DESC") + check ast.stmts[0].selOrderBy.len == 2 + + test "Parse INNER JOIN": + let ast = parse("SELECT u.name, o.total FROM users u INNER JOIN orders o ON u.id = o.user_id") + check ast.stmts[0].selJoins.len == 1 + check ast.stmts[0].selJoins[0].joinKind == jkInner + + test "Parse LEFT JOIN": + let ast = parse("SELECT u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id") + check ast.stmts[0].selJoins.len == 1 + check ast.stmts[0].selJoins[0].joinKind == jkLeft + + test "Parse multiple JOINs": + let ast = parse("SELECT * FROM a JOIN b ON a.id = b.aid JOIN c ON b.id = c.bid") + check ast.stmts[0].selJoins.len == 2 + + test "Parse CTE (WITH)": + let ast = parse("WITH active AS (SELECT * FROM users WHERE active = true) SELECT * FROM active") + check ast.stmts[0].selWith.len == 1 + check ast.stmts[0].selWith[0][0] == "active" + check ast.stmts[0].selWith[0][2] == false + + test "Parse multiple CTEs": + let ast = parse("WITH a AS (SELECT id FROM t1), b AS (SELECT id FROM t2) SELECT * FROM a") + check ast.stmts[0].selWith.len == 2 + check ast.stmts[0].selWith[0][2] == false + + test "Parse aggregate functions in SELECT": + let ast = parse("SELECT count(*), sum(amount), avg(price), min(age), max(score) FROM orders") + check ast.stmts[0].selResult.len == 5 + + test "Parse CASE expression": + let ast = parse("SELECT CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END FROM users") + check ast.stmts[0].selResult.len == 1 + + test "Parse BETWEEN": + let ast = parse("SELECT * FROM products WHERE price BETWEEN 10 AND 100") + check ast.stmts[0].selWhere != nil + + test "Parse subquery in FROM": + let ast = parse("SELECT * FROM (SELECT id FROM users) AS sub") + check ast.stmts[0].selFrom != nil + + test "Parse UPDATE SET WHERE": + let ast = parse("UPDATE users SET name = 'Alice' WHERE id = 1") + check ast.stmts[0].updSet.len == 1 + + test "Parse DELETE WHERE": + let ast = parse("DELETE FROM users WHERE id = 1") + check ast.stmts[0].delWhere != nil + + test "Parse CREATE TYPE with properties": + let ast = parse("CREATE TYPE Person { name: str, age: int32 }") + check ast.stmts[0].ctName == "Person" + check ast.stmts[0].ctProperties.len == 2 + +suite "Raft Consensus": + test "Create cluster with nodes": + var cluster = newRaftCluster() + cluster.addNode("n1") + cluster.addNode("n2") + cluster.addNode("n3") + check cluster.nodes.len == 3 + check cluster.nodes["n1"].peers.len == 2 + + test "Initial state is follower": + var cluster = newRaftCluster() + cluster.addNode("n1") + check cluster.nodes["n1"].state == rsFollower + + test "Election — single node becomes leader": + var cluster = newRaftCluster() + cluster.addNode("n1") + let node = cluster.nodes["n1"] + node.becomeCandidate() + node.becomeLeader() + check node.isLeader + check node.leaderId == "n1" + + test "Log replication": + var cluster = newRaftCluster() + cluster.addNode("n1") + let node = cluster.nodes["n1"] + node.becomeCandidate() + node.becomeLeader() + let entry = node.appendLog("SET key1 value1") + check entry.index == 1 + check node.logLen == 1 + let entry2 = node.appendLog("SET key2 value2") + check entry2.index == 2 + check node.logLen == 2 + + test "RequestVote handling": + var cluster = newRaftCluster() + cluster.addNode("n1") + cluster.addNode("n2") + let n1 = cluster.nodes["n1"] + let n2 = cluster.nodes["n2"] + let req = RaftMessage(kind: rmkRequestVote, term: 1, senderId: "n2", + lastLogIndex: 0, lastLogTerm: 0) + let reply = n1.handleRequestVote(req) + check reply.success + check n1.votedFor == "n2" + + test "AppendEntries handling": + var cluster = newRaftCluster() + cluster.addNode("n1") + cluster.addNode("n2") + let n2 = cluster.nodes["n2"] + let msg = RaftMessage(kind: rmkAppendEntries, term: 1, senderId: "n1", + prevLogIndex: 0, prevLogTerm: 0, + entries: @[LogEntry(term: 1, index: 1, command: "SET x 1")], + leaderCommit: 0) + let reply = n2.handleAppendEntries(msg) + check reply.success + check n2.logLen == 1 + +suite "Sharding": + test "Hash-based sharding": + var router = newShardRouter(ShardConfig(numShards: 4, strategy: ssHash)) + check router.shardCount == 4 + let s1 = router.getShard("user_1") + let s2 = router.getShard("user_2") + check s1 >= 0 and s1 < 4 + check s2 >= 0 and s2 < 4 + + test "Consistent hashing": + var router = newShardRouter(ShardConfig(numShards: 4, strategy: ssConsistent)) + router.addVirtualNodes(50) + let s = router.getShard("some_key") + check s >= 0 and s < 4 + + test "Range-based sharding": + var router = newShardRouter(ShardConfig(numShards: 3, strategy: ssRange)) + router.setRangeBounds(@[("a", "f"), ("g", "n"), ("o", "z")]) + check router.getShardRange("apple") == 0 + check router.getShardRange("hello") == 1 + check router.getShardRange("top") == 2 + + test "Rebalance assigns nodes": + var router = newShardRouter(ShardConfig(numShards: 3, replicas: 2, strategy: ssHash)) + router.rebalance(@["node1", "node2", "node3"]) + for shard in router.shards: + check shard.nodeIds.len == 2 # 2 replicas + + test "Replicas of key": + var router = newShardRouter(ShardConfig(numShards: 2, replicas: 1, strategy: ssHash)) + router.rebalance(@["n1", "n2"]) + let replicas = router.replicasOf("test_key") + check replicas.len == 1 + + test "Active shard count": + var router = newShardRouter() + check router.activeShardCount == 4 + +suite "Schema Inheritance": + test "Inheritance — merge properties from base": + var s = newSchema() + let base = newType("Base") + base.addProperty("id", "str", required = true) + base.addProperty("created", "datetime") + s.addType("default", base) + + let child = newType("Person") + child.setBases(@["Base"]) + child.addProperty("name", "str", required = true) + s.addType("default", child) + + let resolved = s.resolveInheritance(child) + check resolved.properties.len == 3 # id + created + name + check "id" in resolved.properties + check "name" in resolved.properties + check "created" in resolved.properties + + test "Multi-level inheritance": + var s = newSchema() + let a = newType("A") + a.addProperty("a1", "str") + s.addType("default", a) + + let b = newType("B") + b.setBases(@["A"]) + b.addProperty("b1", "int32") + s.addType("default", b) + + let c = newType("C") + c.setBases(@["B"]) + c.addProperty("c1", "bool") + s.addType("default", c) + + let resolved = s.resolveInheritance(c) + check resolved.properties.len == 3 # a1 + b1 + c1 + + test "Override base property": + var s = newSchema() + let base = newType("Base") + base.addProperty("name", "str") + s.addType("default", base) + + let child = newType("Child") + child.setBases(@["Base"]) + child.addProperty("name", "text") # override + s.addType("default", child) + + let resolved = s.resolveInheritance(child) + check resolved.properties["name"].typeName == "text" + + test "isSubtype": + var s = newSchema() + let a = newType("A") + s.addType("default", a) + let b = newType("B") + b.setBases(@["A"]) + s.addType("default", b) + let c = newType("C") + c.setBases(@["B"]) + s.addType("default", c) + + check s.isSubtype("C", "A") + check s.isSubtype("C", "B") + check s.isSubtype("B", "A") + check not s.isSubtype("A", "C") + + test "Computed property": + let t = newType("Person") + t.addProperty("firstName", "str") + t.addProperty("lastName", "str") + t.addComputedProperty("fullName", "str", "firstName ++ ' ' ++ lastName") + check t.properties["fullName"].computed + check t.properties["fullName"].expr == "firstName ++ ' ' ++ lastName" + +suite "Codegen": + test "Codegen scan": + let plan = IRPlan(kind: irpkScan, scanTable: "users", scanAlias: "u") + let op = codegenPlan(plan) + check op.kind == sokScan + check op.table == "users" + + test "Codegen filter with point read optimization": + let filterExpr = IRExpr(kind: irekBinary, binOp: irEq, + binLeft: IRExpr(kind: irekField, fieldPath: @["id"]), + binRight: IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkInt64, int64Val: 42))) + let scan = IRPlan(kind: irpkScan, scanTable: "users", scanAlias: "u") + let plan = IRPlan(kind: irpkFilter, filterSource: scan, filterCond: filterExpr) + let op = codegenPlan(plan) + # Should optimize to point read + check op.kind == sokPointRead + + test "Codegen limit": + let scan = IRPlan(kind: irpkScan, scanTable: "t", scanAlias: "t") + let plan = IRPlan(kind: irpkLimit, limitSource: scan, limitCount: 10, limitOffset: 5) + let op = codegenPlan(plan) + check op.limit == 10 + check op.offset == 5 + + test "Cost estimation": + let scan = newStorageOp(sokScan) + check estimateCost(scan) == 1000.0 + let pointRead = newStorageOp(sokPointRead) + check estimateCost(pointRead) == 1.0 + + test "Explain plan": + let scan = newStorageOp(sokScan) + scan.table = "users" + let explanation = scan.explain() + check "sokScan" in explanation + check "users" in explanation + +suite "Replication": + test "Create replication manager": + var rm = newReplicationManager(rmAsync) + check rm.totalReplicaCount == 0 + check rm.connectedReplicaCount == 0 + + test "Add and connect replicas": + var rm = newReplicationManager(rmAsync) + rm.addReplica(newReplica("r1", "10.0.0.1", 9472)) + rm.addReplica(newReplica("r2", "10.0.0.2", 9472)) + check rm.totalReplicaCount == 2 + + rm.connectReplica("r1") + check rm.connectedReplicaCount == 1 + + test "Async replication — write returns immediately": + var rm = newReplicationManager(rmAsync) + rm.addReplica(newReplica("r1", "10.0.0.1", 9472)) + rm.connectReplica("r1") + + let lsn = rm.writeLsn(@[1'u8, 2, 3]) + check lsn == 1 + # Async doesn't wait — already "acked" + check rm.isFullyAcked(lsn) + + test "Sync replication — wait for ack": + var rm = newReplicationManager(rmSync) + rm.addReplica(newReplica("r1", "10.0.0.1", 9472)) + rm.connectReplica("r1") + + let lsn = rm.writeLsn(@[1'u8, 2, 3]) + check not rm.isFullyAcked(lsn) + + rm.ackLsn("r1", lsn) + check rm.isFullyAcked(lsn) + + test "Semi-sync replication": + var rm = newReplicationManager(rmSemiSync, syncCount = 2) + rm.addReplica(newReplica("r1", "10.0.0.1", 9472)) + rm.addReplica(newReplica("r2", "10.0.0.2", 9472)) + rm.addReplica(newReplica("r3", "10.0.0.3", 9472)) + rm.connectReplica("r1") + rm.connectReplica("r2") + rm.connectReplica("r3") + + let lsn = rm.writeLsn(@[1'u8]) + check not rm.isFullyAcked(lsn) # needs 2 acks + + rm.ackLsn("r1", lsn) + check not rm.isFullyAcked(lsn) # still needs 1 more + + rm.ackLsn("r2", lsn) + check rm.isFullyAcked(lsn) # 2 acks received + + test "Replica status": + var rm = newReplicationManager(rmAsync) + rm.addReplica(newReplica("r1", "10.0.0.1", 9472)) + rm.connectReplica("r1") + let status = rm.replicaStatus() + check status.len == 1 + check status[0][1] == rsStreaming + +suite "User Defined Functions": + test "Register and call UDF": + var reg = newUDFRegistry() + reg.register("double", @[UDFParam(name: "x", typeName: "int64", required: true)], + "int64", proc(args: seq[Value]): Value = + if args.len > 0 and args[0].kind == vkInt64: + return Value(kind: vkInt64, int64Val: args[0].int64Val * 2) + return Value(kind: vkNull)) + + check reg.hasFunction("double") + let result = reg.call("double", @[Value(kind: vkInt64, int64Val: 21)]) + check result.kind == vkInt64 + check result.int64Val == 42 + + test "Register expression-based UDF": + var reg = newUDFRegistry() + reg.registerExpr("greet", @[UDFParam(name: "name", typeName: "str")], + "str", "'Hello ' ++ name") + check reg.hasFunction("greet") + check reg.getFunction("greet").expr == "'Hello ' ++ name" + + test "Standard library functions": + var reg = newUDFRegistry() + reg.registerStdlib() + + # lower + let r1 = reg.call("lower", @[Value(kind: vkString, strVal: "HELLO")]) + check r1.strVal == "hello" + + # upper + let r2 = reg.call("upper", @[Value(kind: vkString, strVal: "hello")]) + check r2.strVal == "HELLO" + + # len + let r3 = reg.call("len", @[Value(kind: vkString, strVal: "test")]) + check r3.int64Val == 4 + + # trim + let r4 = reg.call("trim", @[Value(kind: vkString, strVal: " hello ")]) + check r4.strVal == "hello" + + # toString + let r5 = reg.call("toString", @[Value(kind: vkInt64, int64Val: 42)]) + check r5.strVal == "42" + + test "Deregister function": + var reg = newUDFRegistry() + reg.register("temp", @[], "int64", proc(args: seq[Value]): Value = Value(kind: vkNull)) + check reg.hasFunction("temp") + reg.deregister("temp") + check not reg.hasFunction("temp") + + test "Function count": + var reg = newUDFRegistry() + reg.registerStdlib() + check reg.functionCount > 10 + +suite "Vector SIMD": + test "Dot product": + let a = @[1.0'f32, 2.0'f32, 3.0'f32] + let b = @[4.0'f32, 5.0'f32, 6.0'f32] + let result = dotProductSimd(a, b) + check abs(result - 32.0) < 0.001 + + test "L2 distance": + let a = @[0.0'f32, 0.0'f32] + let b = @[3.0'f32, 4.0'f32] + let result = l2NormSimd(a, b) + check abs(result - 5.0) < 0.001 + + test "Cosine distance": + let a = @[1.0'f32, 0.0'f32, 0.0'f32] + let b = @[0.0'f32, 1.0'f32, 0.0'f32] + let result = cosineSimd(a, b) + check abs(result - 1.0) < 0.001 # orthogonal = 1.0 + + let c = @[1.0'f32, 0.0'f32, 0.0'f32] + let d = @[1.0'f32, 0.0'f32, 0.0'f32] + check cosineSimd(c, d) < 0.001 # same direction = 0.0 + + test "Manhattan distance": + let a = @[1.0'f32, 2.0'f32] + let b = @[4.0'f32, 6.0'f32] + let result = manhattanSimd(a, b) + check abs(result - 7.0) < 0.001 + + test "Normalize vector": + let v = @[3.0'f32, 4.0'f32] + let n = normalize(v) + check abs(n[0] - 0.6) < 0.001 + check abs(n[1] - 0.8) < 0.001 + + test "Add vectors": + let a = @[1.0'f32, 2.0'f32] + let b = @[3.0'f32, 4.0'f32] + let c = addVectors(a, b) + check c[0] == 4.0 + check c[1] == 6.0 + + test "Scale vector": + let v = @[1.0'f32, 2.0'f32, 3.0'f32] + let s = scaleVector(v, 2.0) + check s[0] == 2.0 + check s[1] == 4.0 + check s[2] == 6.0 + + test "TopK": + let distances = @[5.0'f32, 1.0'f32, 3.0'f32, 2.0'f32, 4.0'f32] + let top = topK(distances, 3) + check top.len == 3 + check top[0][0] == 1 # index 1, value 1.0 + check top[1][0] == 3 # index 3, value 2.0 + check top[2][0] == 2 # index 2, value 3.0 + + test "Batch distance": + let queries = @[@[1.0'f32, 0.0'f32], @[0.0'f32, 1.0'f32]] + let corpus = @[@[1.0'f32, 0.0'f32], @[0.0'f32, 1.0'f32], @[1.0'f32, 1.0'f32]] + let results = batchDistance(queries, corpus, "cosine") + check results.len == 2 + check results[0].len == 3 + +suite "Cross-Modal Engine": + test "Create engine": + let engine = newCrossModalEngine("/tmp/baradb_test_crossmodal") + check engine != nil + + test "Document operations": + let engine = newCrossModalEngine("/tmp/baradb_test_crossmodal2") + engine.put("key1", cast[seq[byte]]("value1")) + let (found, val) = engine.get("key1") + check found + check cast[string](val) == "value1" + + test "Vector operations": + let engine = newCrossModalEngine("/tmp/baradb_test_crossmodal3") + engine.insertVector(1, @[1.0'f32, 0.0'f32, 0.0'f32], {"cat": "A"}.toTable) + engine.insertVector(2, @[0.0'f32, 1.0'f32, 0.0'f32], {"cat": "B"}.toTable) + let results = engine.searchVector(@[1.0'f32, 0.1'f32, 0.0'f32], 2) + check results.len == 2 + + test "Graph operations": + let engine = newCrossModalEngine("/tmp/baradb_test_crossmodal4") + let n1 = engine.addNode("Person") + let n2 = engine.addNode("Person") + discard engine.addEdge(n1, n2, "knows") + let traversal = engine.traverseGraph(n1, "bfs") + check traversal.len >= 1 + + test "FTS operations": + let engine = newCrossModalEngine("/tmp/baradb_test_crossmodal5") + engine.indexText(1, "Nim programming language") + engine.indexText(2, "Python data science") + let results = engine.searchText("programming") + check results.len >= 1 + + test "2PC transaction": + var txn = newTPCTransaction(1) + txn.addParticipant("storage") + txn.addParticipant("vector") + txn.addParticipant("graph") + check txn.participantCount == 3 + check txn.prepare() + check txn.isPrepared + check txn.commit() + check txn.isCommitted + + test "2PC rollback": + var txn = newTPCTransaction(2) + txn.addParticipant("storage") + txn.addParticipant("vector") + check txn.prepare() + check txn.rollback() + check txn.isAborted + + test "Hybrid query": + let engine = newCrossModalEngine("/tmp/baradb_test_crossmodal6") + engine.insertVector(1, @[1.0'f32, 0.0'f32], {"cat": "A"}.toTable) + engine.indexText(1, "fast database") + var query = newCrossModalQuery(qmHybrid) + query.vector = @[1.0'f32, 0.0'f32] + query.vectorK = 5 + query.searchQuery = "fast" + query.vecWeight = 1.0 + query.ftsWeight = 1.0 + let result = engine.hybridSearch(query) + check result.totalResults >= 0 + +suite "Gossip Protocol": + test "Create gossip node": + var gp = newGossipProtocol("node1", "10.0.0.1", 7946) + check gp.self.id == "node1" + check gp.memberCount == 0 + + test "Add members": + var gp = newGossipProtocol("node1", "10.0.0.1", 7946) + let node2 = newGossipNode("node2", "10.0.0.2", 7946) + let node3 = newGossipNode("node3", "10.0.0.3", 7946) + gp.addMember(node2) + gp.addMember(node3) + check gp.memberCount == 2 + check gp.aliveCount == 2 + + test "Suspect and declare dead": + var gp = newGossipProtocol("node1", "10.0.0.1", 7946) + let node2 = newGossipNode("node2", "10.0.0.2", 7946) + gp.addMember(node2) + gp.suspect("node2") + check gp.getMember("node2").state == nsSuspect + gp.declareDead("node2") + check gp.memberCount == 1 + + test "Gossip message": + var gp = newGossipProtocol("node1", "10.0.0.1", 7946) + let node2 = newGossipNode("node2", "10.0.0.2", 7946) + gp.addMember(node2) + let msg = gp.createGossipMessage() + check msg.senderId == "node1" + check msg.nodes.len == 1 + + test "Select gossip targets": + var gp = newGossipProtocol("node1", "10.0.0.1", 7946, fanout = 2) + for i in 2..5: + gp.addMember(newGossipNode("node" & $i, "10.0.0." & $i, 7946)) + let targets = gp.selectGossipTargets() + check targets.len <= 2 + + test "Member IDs": + var gp = newGossipProtocol("node1", "10.0.0.1", 7946) + gp.addMember(newGossipNode("node2", "10.0.0.2", 7946)) + check gp.isMember("node2") + check not gp.isMember("node99") + +suite "Client Library": + test "Connection string parser": + let config = parseConnectionString("host=localhost port=9472 dbname=test user=admin") + check config.host == "localhost" + check config.port == 9472 + check config.database == "test" + check config.username == "admin" + + test "Client config defaults": + let config = defaultClientConfig() + check config.host == "127.0.0.1" + check config.port == 9472 + + test "Query builder": + let client = newBaraClient() + let qb = newQueryBuilder(client) + let sql = qb.select("name", "age").from("users") + .where("age > 18").orderBy("name", "ASC").limit(10).build() + check sql == "SELECT name, age FROM users WHERE age > 18 ORDER BY name ASC LIMIT 10" + + test "Query builder with JOIN": + let client = newBaraClient() + let qb = newQueryBuilder(client) + let sql = qb.select("u.name", "o.total") + .from("users u").join("orders o", "u.id = o.user_id") + .where("o.total > 100").build() + check "JOIN" in sql + check "WHERE" in sql + + test "Query builder with GROUP BY": + let client = newBaraClient() + let qb = newQueryBuilder(client) + let sql = qb.select("dept", "count(*)").from("employees") + .groupBy("dept").having("count(*) > 5").build() + check "GROUP BY" in sql + check "HAVING" in sql + +suite "Import/Export": + test "JSON export": + let columns = @["name", "age"] + let rows = @[@["Alice", "30"], @["Bob", "25"]] + let json = fileops.toJson(columns, rows) + check json.startsWith("[") + check "Alice" in json + + test "CSV export": + let columns = @["name", "age"] + let rows = @[@["Alice", "30"], @["Bob", "25"]] + let csv = fileops.toCsv(columns, rows) + check csv.startsWith("name,age") + check "Alice" in csv + + test "JSON import": + let json = """[{"name": "Alice", "age": "30"}, {"name": "Bob", "age": "25"}]""" + let (columns, rows) = fileops.parseJsonTable(json) + check columns.len == 2 + check rows.len == 2 + + test "CSV import": + let csv = "name,age\nAlice,30\nBob,25" + let (columns, rows) = fileops.parseCsvTable(csv) + check columns.len == 2 + check rows.len == 2 + check rows[0][0] == "Alice" + + test "NDJSON export/import": + let columns = @["name", "age"] + let rows = @[@["Alice", "30"]] + let ndjson = fileops.toNdjson(columns, rows) + check "Alice" in ndjson + + test "CSV with quoted fields": + let csv = "name,bio\nAlice,\"Software engineer, Nim\"\nBob,Data scientist" + let (columns, rows) = fileops.parseCsvTable(csv) + check rows.len == 2 + +suite "Multi-Language FTS": + test "English tokenizer": + let config = mlang.getLanguageConfig(mlang.langEnglish) + let tokens = mlang.tokenize("The quick brown fox jumps over the lazy dog", config) + check tokens.len > 0 + check "the" notin tokens # stop word + + test "Bulgarian tokenizer": + let config = mlang.getLanguageConfig(mlang.langBulgarian) + let tokens = mlang.tokenize("Бързата кафява лисица прескача мързеливото куче", config) + check tokens.len > 0 + + test "German tokenizer": + let config = mlang.getLanguageConfig(mlang.langGerman) + let tokens = mlang.tokenize("Der schnelle braune Fuchs springt über den faulen Hund", config) + check tokens.len > 0 + check "der" notin tokens + + test "Russian tokenizer": + let config = mlang.getLanguageConfig(mlang.langRussian) + let tokens = mlang.tokenize("Быстрая браун лиса прыгает через ленивую собаку", config) + check tokens.len > 0 + + test "Language detection": + check mlang.detectLanguage("Hello world how are you") == mlang.langEnglish + # Bulgarian text is also Cyrillic — detected as Russian by default + check mlang.detectLanguage("Здравей свят как си") == mlang.langRussian + + test "English stemming": + check mlang.stemEnglish("running") == "runn" + check mlang.stemEnglish("cats") == "cat" + check mlang.stemEnglish("programming") == "programm" + + test "Bulgarian stemming": + check mlang.stemBulgarian("красота") == "красо" + +suite "Zero-Copy Serialization": + test "Write and read int32": + var buf = newZeroBuf(64) + buf.writeInt32(42) + check buf.readInt32(0) == 42 + buf.free() + + test "Write and read int64": + var buf = newZeroBuf(64) + buf.writeInt64(12345) + check buf.readInt64(0) == 12345 + buf.free() + + test "Write and read bool": + var buf = newZeroBuf(64) + buf.writeBool(true) + check buf.readBool(0) + buf.free() + + test "ZcSchema field offsets": + var schema = newZcSchema("user") + schema.addField("id", ztInt64) + schema.addField("name", ztString) + check schema.fields.len == 2 + check schema.totalSize > 0 + + test "Encode and decode record": + var schema = newZcSchema("user") + schema.addField("id", ztInt32) + var buf = newZeroBuf(schema.totalSize) + buf.pos = schema.totalSize # pretend we wrote + buf.encodeRecord(schema, {"id": "42"}.toTable) + # Reset pos for reading at offsets + var pos = 0 + let row = buf.decodeRecord(schema) + check row["id"] == "42" + buf.free() + + test "ZcTable batch operations": + var schema = newZcSchema("user") + schema.addField("id", ztInt32) + var table = newZcTable(schema) + var buf1 = newZeroBuf(schema.totalSize) + buf1.encodeRecord(schema, {"id": "1"}.toTable) + table.records.add(buf1) + var buf2 = newZeroBuf(schema.totalSize) + buf2.encodeRecord(schema, {"id": "2"}.toTable) + table.records.add(buf2) + table.totalRows = 2 + check table.totalRows == 2 + check table.getRecord(1)["id"] == "2" + for i in 0..