diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml deleted file mode 100644 index edc8a55..0000000 --- a/.github/workflows/debug.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Debug Tests - -on: - push: - branches: [main] - -jobs: - debug: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Nim - uses: jiro4989/setup-nim-action@v1 - with: - nim-version: '2.2.10' - - - name: Install system dependencies - run: sudo apt-get update -qq && sudo apt-get install -y -qq libssl-dev libpcre3-dev openssl ca-certificates - - - name: Install Nim dependencies - run: nimble install --depsOnly -y - - - name: Check nim binary - run: | - which nim - nim --version - echo "NIM_OK=1" - - - name: Compile and run lock test - run: nim c --threads:on --path:src -r tests/test_lock.nim - - - name: Run join_tests - run: timeout 30 nim c -d:ssl --threads:on --path:src -r tests/join_tests.nim - - - name: Run tla_faithfulness - run: nim c -d:ssl --threads:on --path:src -r tests/tla_faithfulness.nim - - - name: Compile test_all - run: nim c -d:ssl --threads:on --path:src tests/test_all.nim - - - name: Run test_all binary - run: ./tests/test_all > test_all.log 2>&1 - - - name: Upload test_all log to repo - if: failure() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - URL=$(curl -s -F'file=@test_all.log' https://0x0.st) - echo "::notice::test_all.log uploaded to $URL" - sudo apt-get install -y -qq git - git config user.name "CI Bot" - git config user.email "ci@baradb" - git checkout -b ci-logs-${{ github.run_id }} || true - cp test_all.log ci_log.txt - git add ci_log.txt - git commit -m "CI log for run ${{ github.run_id }}" || true - git push https://$GITHUB_TOKEN@github.com/${{ github.repository }}.git ci-logs-${{ github.run_id }} || echo "PUSH_FAILED=1" diff --git a/Dockerfile b/Dockerfile index 4fa1f4b..aec3e9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ FROM debian:bookworm-slim LABEL maintainer="BaraDB Team" LABEL description="BaraDB — Multimodal Database Engine" -LABEL version="1.0.0" +LABEL version="1.1.0" # Инсталираме runtime зависимости # libpcre3 — нужна за Nim regex (зарежда се динамично) diff --git a/Dockerfile.source b/Dockerfile.source index 40fa9bb..96603a1 100644 --- a/Dockerfile.source +++ b/Dockerfile.source @@ -40,7 +40,7 @@ FROM alpine:3.19 LABEL maintainer="BaraDB Team" LABEL description="BaraDB — Multimodal Database Engine (source build)" -LABEL version="1.0.0" +LABEL version="1.1.0" # Инсталираме runtime зависимости RUN apk add --no-cache ca-certificates su-exec wget pcre diff --git a/README.md b/README.md index 86582f6..1ad8403 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **A multimodal database engine written in Nim — 100% native, zero dependencies.** -[![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)](baradadb.nimble) +[![Version](https://img.shields.io/badge/version-1.1.0-blue.svg)](baradadb.nimble) [![Documentation](https://img.shields.io/badge/docs-7_languages-blue.svg)](docs/index.md) ## Documentation diff --git a/baradadb.nimble b/baradadb.nimble index 9c53dce..8f2b601 100644 --- a/baradadb.nimble +++ b/baradadb.nimble @@ -1,5 +1,5 @@ # Package -version = "1.0.0" +version = "1.1.0" author = "BaraDB Team" description = "BaraDB — Multimodal database written in Nim" license = "Apache-2.0" diff --git a/clients/javascript/package.json b/clients/javascript/package.json index 4ca5ab3..25b1a45 100644 --- a/clients/javascript/package.json +++ b/clients/javascript/package.json @@ -1,6 +1,6 @@ { "name": "baradb", - "version": "1.0.0", + "version": "1.1.0", "description": "Official JavaScript/Node.js client for BaraDB — Multimodal Database Engine", "main": "baradb.js", "types": "baradb.d.ts", diff --git a/clients/nim/baradb.nimble b/clients/nim/baradb.nimble index f2aa1f0..5701aca 100644 --- a/clients/nim/baradb.nimble +++ b/clients/nim/baradb.nimble @@ -1,6 +1,6 @@ # Package -version = "1.0.0" +version = "1.1.0" author = "BaraDB Team" description = "Official Nim client for BaraDB — async binary protocol client" license = "Apache-2.0" diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml index c981b17..9c84c3a 100644 --- a/clients/python/pyproject.toml +++ b/clients/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "baradb" -version = "1.0.0" +version = "1.1.0" description = "Official Python client for BaraDB — Multimodal Database Engine" readme = "README.md" license = { text = "Apache-2.0" } diff --git a/clients/rust/Cargo.toml b/clients/rust/Cargo.toml index 75c866f..e4a3324 100644 --- a/clients/rust/Cargo.toml +++ b/clients/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "baradb" -version = "1.0.0" +version = "1.1.0" edition = "2021" authors = ["BaraDB Team "] description = "Official Rust client for BaraDB — binary protocol client" diff --git a/docs/en/changelog.md b/docs/en/changelog.md index dce1463..a60ec2d 100644 --- a/docs/en/changelog.md +++ b/docs/en/changelog.md @@ -2,6 +2,45 @@ All notable changes to BaraDB are documented in this file. +## [1.1.0] — 2026-05-13 + +### Added + +- **Client SDKs v1.1.0** — Full-featured clients for all languages: + - JavaScript: TypeScript definitions, package.json, examples, unit & integration tests + - Python: Restructured as proper package (`baradb/` with `__init__.py` and `core.py`), pyproject.toml, examples, tests (query builder, wire protocol, integration) + - Nim: Examples, integration tests, README + - Rust: Examples, integration tests, improved Cargo.toml +- **SCRAM-SHA-256 Authentication** — RFC 7677 compliant authentication with PBKDF2 + HMAC + SHA-256 + nonce/salt generation +- **HTTP SCRAM Endpoints** — `/auth/scram/start` + `/auth/scram/finish` in HTTP server +- **Docker Compose Test Configuration** — `docker-compose.test.yml` for test environments +- **CI/CD Clients Pipeline** — `.github/workflows/clients-ci.yml` for automated client testing + +### Fixed + +- **Query Executor** — Unary minus (`irNeg`) evaluation now works correctly in SELECT and WHERE clauses +- **Distributed Transactions** — Rollback after commit attempt no longer violates atomicity +- **Sharding** — Data migration protocol with TCP + `scanAll` on LSM +- **Raft** — Majority calculation for even number of nodes fixed +- **MVCC** — Aborted transactions no longer become visible +- **LSM-Tree** — Data loss on immutable memtable overwrite fixed; SSTable lookup sorting fixed +- **Auth** — JWT signature changed to HMAC-SHA256 (no longer trivially forgeable); token expiration (`exp`/`nbf`/`iat`) now validated; signature comparison is now constant-time +- **Recovery** — `summary()` no longer mutates the database +- **Wire Protocol** — 64MB limit + bounds checking + max depth to prevent OOM/DoS +- **SQL Injection** — `exprToSql` now escapes single quotes +- **ReDoS** — `irLike`/`irILike` now escape regex metacharacters +- **Graph** — `addEdge` now checks node existence +- **Vector** — Dimension mismatch validation + HNSW locking +- **FTS** — UTF-8 tokenization now uses runes instead of bytes +- **Build** — `nim.cfg` adds `-d:ssl` so `nimble build` works without flags; `--threads:on` added to all CI commands + +### Changed + +- **Version bumped to 1.1.0** across all components (server, Docker images, clients, CLI) +- **README** — Version badge updated; all feature tables now reference v1.1.0 +- **TLA+ Formal Verification** — Added `crossmodal.tla`, `backup.tla`, `recovery.tla`; symmetry reduction in all 9 specs +- **Clean build** — 0 compiler warnings on Nim 2.2.10 + ## [0.1.0] — 2025-01-15 ### Added diff --git a/src/barabadb/cli/shell.nim b/src/barabadb/cli/shell.nim index 88d7698..128a160 100644 --- a/src/barabadb/cli/shell.nim +++ b/src/barabadb/cli/shell.nim @@ -5,7 +5,7 @@ import ../query/lexer import ../query/parser const - Version = "0.1.0" + Version = "1.1.0" Prompt = "bara> " ContinuationPrompt = " .. > " diff --git a/src/barabadb/core/httpserver.nim b/src/barabadb/core/httpserver.nim index f6e8711..9f89e56 100644 --- a/src/barabadb/core/httpserver.nim +++ b/src/barabadb/core/httpserver.nim @@ -178,7 +178,7 @@ proc queryHandler(server: HttpServer): RequestHandler = proc healthHandler(): RequestHandler = return proc(request: Request) {.gcsafe.} = let ctx = newContext(request) - ctx.json(%*{"status": "ok", "version": "0.1.0"}) + ctx.json(%*{"status": "ok", "version": "1.1.0"}) proc metricsHandler(server: HttpServer): RequestHandler = return proc(request: Request) {.gcsafe.} = @@ -514,7 +514,7 @@ function showTab(idx){ } setInterval(() => { if(document.querySelectorAll('.panel')[4].classList.contains('active')) loadMetrics() }, 5000) -
BaraDB v1.0.0 — Multimodal Database Engine
+
BaraDB v1.1.0 — Multimodal Database Engine
""" request.respond(200, @[("Content-Type", "text/html; charset=utf-8")], html) diff --git a/src/barabadb/core/tracing.nim b/src/barabadb/core/tracing.nim index e687e31..dfe438f 100644 --- a/src/barabadb/core/tracing.nim +++ b/src/barabadb/core/tracing.nim @@ -116,7 +116,7 @@ proc exportOtlp*(tracer: Tracer, endpoint: string = "http://localhost:4318/v1/tr {"key": "service.name", "value": {"stringValue": "baradadb"}} ]}, "scopeSpans": [{ - "scope": {"name": "baradadb-tracer", "version": "0.1.0"}, + "scope": {"name": "baradadb-tracer", "version": "1.1.0"}, "spans": otlpSpans }] }] diff --git a/src/baradadb.nim b/src/baradadb.nim index e78f54d..73b8a22 100644 --- a/src/baradadb.nim +++ b/src/baradadb.nim @@ -86,7 +86,7 @@ proc main() = # Init structured logger from config let logLvl = parseEnum[LogLevel]("ll" & capitalizeAscii(config.logLevel)) defaultLogger = newLogger(logLvl, config.logFile) - info("BaraDB v1.0.0 — Multimodal Database Engine") + info("BaraDB v1.1.0 — Multimodal Database Engine") # Security check: warn if JWT secret is not configured if config.jwtSecret.len == 0: diff --git a/tests/test_all.nim.bak b/tests/test_all.nim.bak deleted file mode 100644 index 2942370..0000000 --- a/tests/test_all.nim.bak +++ /dev/null @@ -1,2571 +0,0 @@ -## BaraDB — Test Suite -import std/unittest -import std/tables -import std/strutils -import std/os -import std/asyncdispatch -import std/times -import std/random -import std/monotimes -import std/base64 - -import barabadb/core/types -import barabadb/core/mvcc -import barabadb/core/deadlock -import barabadb/core/config -import barabadb/core/server -import barabadb/core/columnar -import barabadb/core/raft -import barabadb/core/sharding -import barabadb/core/replication -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/query/codegen -import barabadb/query/udf -import barabadb/vector/simd -import barabadb/core/crossmodal -import barabadb/core/gossip -import barabadb/client/client -import barabadb/client/fileops -import barabadb/fts/multilang as mlang -import barabadb/protocol/zerocopy -import barabadb/query/adaptive -import barabadb/query/executor as qexec -import barabadb/core/disttxn -import barabadb/vector/engine as vengine -import barabadb/graph/cypher -import barabadb/vector/quant as vquant -import barabadb/storage/recovery -import barabadb/cli/shell -import barabadb/protocol/ssl -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/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.. 0 - check cypher.returnExprs.len == 2 - check cypher.orderBy.len > 0 - check cypher.limit == 10 - - test "Execute basic MATCH": - var g = newGraph() - discard g.addNode("Person", {"name": "Alice"}.toTable) - discard g.addNode("Person", {"name": "Bob"}.toTable) - discard g.addNode("Company", {"name": "Acme"}.toTable) - - let query = parseCypher("MATCH (p:Person) RETURN p") - let result = executeCypher(g, query) - check result.rows.len == 2 - - test "Match nodes with properties": - var g = newGraph() - discard g.addNode("Person", {"name": "Alice", "age": "30"}.toTable) - discard g.addNode("Person", {"name": "Bob", "age": "25"}.toTable) - - let matches = matchNodes(g, "Person", {"name": "Alice"}.toTable) - check matches.len == 1 - check matches[0].properties["name"] == "Alice" - -suite "Crash Recovery": - test "Scan WAL file": - var walDir = "/tmp/baradb_test_recovery_wal" - var wal = newWriteAheadLog(walDir) - wal.writePut(@[1'u8], @[2'u8], 1) - wal.sync() - wal.close() - - var rec = newCrashRecovery(walDir, "/tmp") - let entries = rec.scanWAL() - check entries.len >= 1 # at least the put entry - - test "Analyze recovery": - var walDir = "/tmp/baradb_test_recovery_wal2" - var wal = newWriteAheadLog(walDir) - wal.writePut(@[1'u8], @[2'u8], 1) - wal.writeCommit(1) - wal.close() - - var rec = newCrashRecovery(walDir, "/tmp") - let result = rec.analyze() - check result.totalEntries >= 1 - check result.applied == true - - test "Recover returns summary": - var walDir = "/tmp/baradb_test_recovery_wal3" - var wal = newWriteAheadLog(walDir) - wal.writePut(@[1'u8], @[2'u8], 1) - wal.writeCommit(1) - wal.close() - - var rec = newCrashRecovery(walDir, "/tmp") - let summary = rec.summary() - check "WAL Recovery" in summary - check "Total" in summary - -suite "Raft Election Timer": - test "Election timer tick": - var cluster = newRaftCluster() - cluster.addNode("n1") - cluster.addNode("n2") - cluster.addNode("n3") - - let n1 = cluster.nodes["n1"] - var timer = newElectionTimer(n1, timeoutMs = 0) # immediate timeout - - # Force election - n1.becomeCandidate() - n1.becomeLeader() - timer.tick() - check n1.isLeader - - test "Timer reset": - var cluster = newRaftCluster() - cluster.addNode("n1") - let n1 = cluster.nodes["n1"] - var timer = newElectionTimer(n1, timeoutMs = 1000) - timer.resetTimeout() - check not timer.checkTimeout() - - test "Multi-node election with timer": - var cluster = newRaftCluster() - cluster.addNode("n1") - cluster.addNode("n2") - - let n2 = cluster.nodes["n2"] - var timer2 = newElectionTimer(n2, timeoutMs = 0) - n2.becomeCandidate() - let req = n2.requestVote() - check req.len == 1 # n2 requests vote from n1 - let reply = cluster.nodes["n1"].handleRequestVote(req[0]) - check reply.success - -suite "Raft Network Transport": - test "3-node election over TCP": - var n1 = newRaftNode("n1", @["n2", "n3"], raftPort = 19001) - var n2 = newRaftNode("n2", @["n1", "n3"], raftPort = 19002) - var n3 = newRaftNode("n3", @["n1", "n2"], raftPort = 19003) - - n1.peerAddrs["n2"] = ("127.0.0.1", 19002) - n1.peerAddrs["n3"] = ("127.0.0.1", 19003) - n2.peerAddrs["n1"] = ("127.0.0.1", 19001) - n2.peerAddrs["n3"] = ("127.0.0.1", 19003) - n3.peerAddrs["n1"] = ("127.0.0.1", 19001) - n3.peerAddrs["n2"] = ("127.0.0.1", 19002) - - let net1 = newRaftNetwork(n1) - let net2 = newRaftNetwork(n2) - let net3 = newRaftNetwork(n3) - - asyncCheck net1.run() - asyncCheck net2.run() - asyncCheck net3.run() - waitFor sleepAsync(50) - - var timer1 = newElectionTimer(n1, timeoutMs = 50) - var timer2 = newElectionTimer(n2, timeoutMs = 100) - var timer3 = newElectionTimer(n3, timeoutMs = 150) - - for i in 0 ..< 30: - timer1.tick(net1) - timer2.tick(net2) - timer3.tick(net3) - waitFor sleepAsync(20) - - net1.stop() - net2.stop() - net3.stop() - waitFor sleepAsync(50) - - var leaderCount = 0 - if n1.isLeader: inc leaderCount - if n2.isLeader: inc leaderCount - if n3.isLeader: inc leaderCount - check leaderCount == 1 - -suite "CLI Autocomplete": - test "Autocomplete commands": - let res = autocomplete("he") - check "help" in res - - test "Autocomplete keywords": - let res = autocomplete("SEL") - check "SELECT" in res - let res2 = autocomplete("SELECT FRO") - check "FROM" in res2 - - test "Suggest returns completions": - let suggestion = suggest("SE") - check suggestion.len > 0 - - test "Autocomplete empty": - let res = autocomplete("") - check res.len == 0 - -suite "TLS/SSL": - test "Create TLS config": - let config = newTLSConfig("cert.pem", "key.pem", "ca.pem", verifyPeer = true) - check config.certFile == "cert.pem" - check config.verifyPeer == true - - test "Validate cert — missing file": - let errors = validateCert("nonexistent.pem") - check errors.len > 0 - - test "Certificate info parsing": - # Write a dummy PEM cert - let testCert = "/tmp/baradb_test_cert.pem" - writeFile(testCert, "Subject: CN=localhost\nIssuer: CN=localhost\n") - let info = parseCertInfo(testCert) - check info.subject.len > 0 - check info.isSelfSigned # subject == issuer - - test "TLS context creation with missing cert raises": - var raised = false - try: - discard newTLSContext(newTLSConfig("nonexistent.pem", "nonexistent.key")) - except IOError: - raised = true - check raised - - test "Generate self-signed cert": - let (certPath, keyPath) = generateSelfSignedCert("/tmp/baradb_test_tls", "test.local") - # May fail if openssl not installed - if certPath.len > 0: - check fileExists(certPath) - check fileExists(keyPath) - # Should be able to create TLS context from generated cert - let ctx = newTLSContext(newTLSConfig(certPath, keyPath)) - check ctx != nil - - test "Server with TLS config": - var cfg = defaultConfig() - cfg.tlsEnabled = true - let (certPath, keyPath) = generateSelfSignedCert("/tmp/baradb_test_tls2", "test.local") - if certPath.len > 0: - cfg.certFile = certPath - cfg.keyFile = keyPath - var srv = newServer(cfg) - check srv != nil - check srv.tls != nil - -suite "Triggers": - test "Parse CREATE TRIGGER": - let ast = parse("CREATE TRIGGER log_insert BEFORE INSERT ON users AS INSERT INTO audit_log VALUES ('insert', 'users')") - check ast.stmts.len == 1 - check ast.stmts[0].kind == nkCreateTrigger - check ast.stmts[0].trigName == "log_insert" - check ast.stmts[0].trigTable == "users" - check ast.stmts[0].trigTiming == "before" - check ast.stmts[0].trigEvent == "INSERT" - check ast.stmts[0].trigAction.strVal.contains("INSERT") - check ast.stmts[0].trigAction.strVal.contains("audit_log") - - test "Parse CREATE TRIGGER AFTER UPDATE": - let ast = parse("CREATE TRIGGER audit_update AFTER UPDATE ON orders AS INSERT INTO audit VALUES ('updated')") - check ast.stmts[0].kind == nkCreateTrigger - check ast.stmts[0].trigTiming == "after" - check ast.stmts[0].trigEvent == "UPDATE" - - test "Parse CREATE TRIGGER INSTEAD OF DELETE": - let ast = parse("CREATE TRIGGER soft_delete INSTEAD OF DELETE ON users AS UPDATE users SET deleted = true WHERE id = OLD.id") - check ast.stmts[0].kind == nkCreateTrigger - check ast.stmts[0].trigTiming == "instead of" - check ast.stmts[0].trigEvent == "DELETE" - - test "Parse DROP TRIGGER": - let ast = parse("DROP TRIGGER log_insert") - check ast.stmts.len == 1 - check ast.stmts[0].kind == nkDropTrigger - check ast.stmts[0].trigDropName == "log_insert" - check ast.stmts[0].trigDropIfExists == false - - test "Parse DROP TRIGGER IF EXISTS": - let ast = parse("DROP TRIGGER IF EXISTS old_trigger") - check ast.stmts[0].kind == nkDropTrigger - check ast.stmts[0].trigDropName == "old_trigger" - check ast.stmts[0].trigDropIfExists == true - -suite "Row-Level Security": - test "Parse CREATE USER": - let ast = parse("CREATE USER admin WITH PASSWORD 'secret' SUPERUSER") - check ast.stmts.len == 1 - check ast.stmts[0].kind == nkCreateUser - check ast.stmts[0].cuName == "admin" - check ast.stmts[0].cuPassword == "secret" - check ast.stmts[0].cuSuperuser == true - - test "Parse CREATE USER without superuser": - let ast = parse("CREATE USER reader WITH PASSWORD 'reader123'") - check ast.stmts[0].kind == nkCreateUser - check ast.stmts[0].cuName == "reader" - check ast.stmts[0].cuPassword == "reader123" - check ast.stmts[0].cuSuperuser == false - - test "Parse DROP USER": - let ast = parse("DROP USER admin") - check ast.stmts[0].kind == nkDropUser - check ast.stmts[0].duName == "admin" - - test "Parse CREATE POLICY": - let ast = parse("CREATE POLICY user_isolation ON accounts FOR SELECT USING (user_id = current_user)") - check ast.stmts[0].kind == nkCreatePolicy - check ast.stmts[0].cpName == "user_isolation" - check ast.stmts[0].cpTable == "accounts" - check ast.stmts[0].cpCommand == "SELECT" - - test "Parse CREATE POLICY with WITH CHECK": - let ast = parse("CREATE POLICY insert_check ON accounts FOR INSERT WITH CHECK (amount > 0)") - check ast.stmts[0].kind == nkCreatePolicy - check ast.stmts[0].cpCommand == "INSERT" - - test "Parse DROP POLICY": - let ast = parse("DROP POLICY user_isolation ON accounts") - check ast.stmts[0].kind == nkDropPolicy - check ast.stmts[0].dpName == "user_isolation" - check ast.stmts[0].dpTable == "accounts" - - test "Parse GRANT": - let ast = parse("GRANT SELECT ON accounts TO reader") - check ast.stmts[0].kind == nkGrant - check ast.stmts[0].grPrivilege == "SELECT" - check ast.stmts[0].grTable == "accounts" - check ast.stmts[0].grGrantee == "reader" - - test "Parse REVOKE": - let ast = parse("REVOKE INSERT ON accounts FROM reader") - check ast.stmts[0].kind == nkRevoke - check ast.stmts[0].rvPrivilege == "INSERT" - check ast.stmts[0].rvTable == "accounts" - check ast.stmts[0].rvGrantee == "reader" - - test "Parse ENABLE ROW LEVEL SECURITY": - let ast = parse("ALTER TABLE accounts ENABLE ROW LEVEL SECURITY") - check ast.stmts[0].kind == nkEnableRLS - check ast.stmts[0].erlsTable == "accounts" - - test "Parse DISABLE ROW LEVEL SECURITY": - let ast = parse("ALTER TABLE accounts DISABLE ROW LEVEL SECURITY") - check ast.stmts[0].kind == nkDisableRLS - check ast.stmts[0].drlsTable == "accounts" - - test "RLS filter on SELECT": - var testDir = getTempDir() / "baradb_rls_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks - createDir(testDir) - var db = newLSMTree(testDir) - var ctx = qexec.newExecutionContext(db) - # Create table and insert data - discard qexec.executeQuery(ctx, parse("CREATE TABLE docs (id INTEGER, owner TEXT)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO docs (id, owner) VALUES (1, 'alice'), (2, 'bob')")) - # Create user and policy - ctx.currentUser = "alice" - ctx.users["alice"] = qexec.UserDef(name: "alice", passwordHash: "", isSuperuser: false, roles: @[]) - ctx.policies["docs"] = @[ - qexec.PolicyDef(name: "owner_only", tableName: "docs", command: "SELECT", - usingExpr: Node(kind: nkBinOp, binOp: bkEq, - binLeft: Node(kind: nkIdent, identName: "owner"), - binRight: Node(kind: nkStringLit, strVal: "alice")), - withCheckExpr: nil) - ] - # Query should only return alice's row - let res = qexec.executeQuery(ctx, parse("SELECT id, owner FROM docs")) - check res.success - check res.rows.len == 1 - check res.rows[0]["owner"] == "alice" - - test "RLS superuser bypass": - var testDir = getTempDir() / "baradb_rls_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks - createDir(testDir) - var db = newLSMTree(testDir) - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE TABLE docs (id INTEGER, owner TEXT)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO docs (id, owner) VALUES (1, 'alice')")) - ctx.currentUser = "admin" - ctx.users["admin"] = qexec.UserDef(name: "admin", passwordHash: "", isSuperuser: true, roles: @[]) - ctx.policies["docs"] = @[ - qexec.PolicyDef(name: "owner_only", tableName: "docs", command: "SELECT", - usingExpr: Node(kind: nkBinOp, binOp: bkEq, - binLeft: Node(kind: nkIdent, identName: "owner"), - binRight: Node(kind: nkStringLit, strVal: "alice")), - withCheckExpr: nil) - ] - let res = qexec.executeQuery(ctx, parse("SELECT id, owner FROM docs")) - check res.success - check res.rows.len == 1 # superuser sees all (only 1 row exists) - -suite "UTF-8 Support": - test "Tokenize UTF-8 identifiers": - let tokens = lex.tokenize("SELECT имя FROM потребители") - check tokens[1].kind == tkIdent - check tokens[1].value == "имя" - check tokens[3].kind == tkIdent - check tokens[3].value == "потребители" - - test "Parse UTF-8 table and column names": - let ast = parse("SELECT имя, възраст FROM потребители WHERE град = 'София'") - check ast.stmts[0].kind == nkSelect - check ast.stmts[0].selFrom.fromTable == "потребители" - check ast.stmts[0].selResult[0].identName == "имя" - check ast.stmts[0].selWhere.whereExpr.binRight.strVal == "София" - - test "Execute query with UTF-8 data": - var db = newLSMTree("") - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE TABLE потребители (имя TEXT, град TEXT)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO потребители (имя, град) VALUES ('Иван', 'София'), ('Мария', 'Пловдив')")) - let res = qexec.executeQuery(ctx, parse("SELECT имя, град FROM потребители WHERE град = 'София'")) - check res.success - check res.rows.len == 1 - check res.rows[0]["имя"] == "Иван" - check res.rows[0]["град"] == "София" - -suite "B-Tree Range Scan": - test "BETWEEN uses index range scan": - var db = newLSMTree("") - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE TABLE products (id INTEGER, name TEXT)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO products (id, name) VALUES (1, 'apple'), (2, 'banana'), (3, 'cherry'), (4, 'date'), (5, 'elderberry')")) - discard qexec.executeQuery(ctx, parse("CREATE INDEX idx_products_name ON products(name)")) - let res = qexec.executeQuery(ctx, parse("SELECT name FROM products WHERE name BETWEEN 'banana' AND 'date'")) - check res.success - check res.rows.len == 3 - - test "Greater than uses index range scan": - var db = newLSMTree("") - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE TABLE nums (id INTEGER, val TEXT)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO nums (id, val) VALUES (1, '10'), (2, '20'), (3, '30'), (4, '40'), (5, '50')")) - discard qexec.executeQuery(ctx, parse("CREATE INDEX idx_nums_val ON nums(val)")) - let res = qexec.executeQuery(ctx, parse("SELECT val FROM nums WHERE val > '20'")) - check res.success - check res.rows.len == 3 - - test "Less than or equal uses index range scan": - var db = newLSMTree("") - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE TABLE nums2 (id INTEGER, val TEXT)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO nums2 (id, val) VALUES (1, '10'), (2, '20'), (3, '30'), (4, '40'), (5, '50')")) - discard qexec.executeQuery(ctx, parse("CREATE INDEX idx_nums2_val ON nums2(val)")) - let res = qexec.executeQuery(ctx, parse("SELECT val FROM nums2 WHERE val <= '30'")) - check res.success - check res.rows.len == 3 - -suite "Enhanced Migrations": - test "Parse CREATE MIGRATION with UP/DOWN": - let ast = parse("CREATE MIGRATION add_users { UP: CREATE TABLE users (id INTEGER PRIMARY KEY); DOWN: DROP TABLE users; }") - check ast.stmts.len == 1 - check ast.stmts[0].kind == nkCreateMigration - check ast.stmts[0].cmName == "add_users" - check ast.stmts[0].cmBody.contains("CREATE TABLE users") - check ast.stmts[0].cmDownBody.contains("DROP TABLE users") - - test "Parse MIGRATION STATUS": - let ast = parse("MIGRATION STATUS") - check ast.stmts[0].kind == nkMigrationStatus - - test "Parse MIGRATION UP": - let ast = parse("MIGRATION UP") - check ast.stmts[0].kind == nkMigrationUp - check ast.stmts[0].muCount == 0 - - test "Parse MIGRATION UP 5": - let ast = parse("MIGRATION UP 5") - check ast.stmts[0].kind == nkMigrationUp - check ast.stmts[0].muCount == 5 - - test "Parse MIGRATION DOWN": - let ast = parse("MIGRATION DOWN") - check ast.stmts[0].kind == nkMigrationDown - check ast.stmts[0].mdCount == 1 - - test "Parse MIGRATION DOWN 3": - let ast = parse("MIGRATION DOWN 3") - check ast.stmts[0].kind == nkMigrationDown - check ast.stmts[0].mdCount == 3 - - test "Parse MIGRATION DRYRUN": - let ast = parse("MIGRATION DRYRUN add_users") - check ast.stmts[0].kind == nkMigrationDryRun - check ast.stmts[0].mdrName == "add_users" - - test "Create and apply migration with checksum": - var testDir = getTempDir() / "baradb_migration_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks - createDir(testDir) - var db = newLSMTree(testDir) - var ctx = qexec.newExecutionContext(db) - # Create migration - let createRes = qexec.executeQuery(ctx, parse("CREATE MIGRATION add_users { UP: CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); DOWN: DROP TABLE users; }")) - check createRes.success - check createRes.message.contains("checksum") - # Apply migration - let applyRes = qexec.executeQuery(ctx, parse("APPLY MIGRATION add_users")) - check applyRes.success - check applyRes.message.contains("ms") - # Check table exists - let tableRes = qexec.executeQuery(ctx, parse("SELECT name FROM users")) - check tableRes.success # table exists (empty result is OK) - # Re-apply should be idempotent - let reapplyRes = qexec.executeQuery(ctx, parse("APPLY MIGRATION add_users")) - check reapplyRes.success - check reapplyRes.message.contains("already applied") - - test "Migration STATUS shows applied migrations": - var testDir = getTempDir() / "baradb_migration_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks - createDir(testDir) - var db = newLSMTree(testDir) - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE MIGRATION m1 { UP: CREATE TABLE t1 (id INTEGER); }")) - discard qexec.executeQuery(ctx, parse("CREATE MIGRATION m2 { UP: CREATE TABLE t2 (id INTEGER); }")) - discard qexec.executeQuery(ctx, parse("APPLY MIGRATION m1")) - let statusRes = qexec.executeQuery(ctx, parse("MIGRATION STATUS")) - check statusRes.success - check statusRes.rows.len == 2 - check statusRes.rows[0]["status"] == "applied" - check statusRes.rows[1]["status"] == "pending" - - test "Migration UP applies all pending": - var testDir = getTempDir() / "baradb_migration_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks - createDir(testDir) - var db = newLSMTree(testDir) - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE MIGRATION m1 { UP: CREATE TABLE t1 (id INTEGER); }")) - discard qexec.executeQuery(ctx, parse("CREATE MIGRATION m2 { UP: CREATE TABLE t2 (id INTEGER); }")) - let upRes = qexec.executeQuery(ctx, parse("MIGRATION UP")) - check upRes.success - check upRes.message.contains("Applied 2 migrations") - - test "Migration DOWN rollback": - var testDir = getTempDir() / "baradb_migration_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks - createDir(testDir) - var db = newLSMTree(testDir) - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE MIGRATION add_t { UP: CREATE TABLE t (id INTEGER); DOWN: DROP TABLE t; }")) - discard qexec.executeQuery(ctx, parse("APPLY MIGRATION add_t")) - let downRes = qexec.executeQuery(ctx, parse("MIGRATION DOWN")) - check downRes.success - check downRes.message.contains("Rolled back 1 migrations") - # After rollback, table should be gone (check by listing tables) - let tableRes = qexec.executeQuery(ctx, parse("SELECT name FROM __tables WHERE name = 't'")) - check tableRes.success - check tableRes.rows.len == 0 # table does not exist - - test "Migration DRYRUN": - var testDir = getTempDir() / "baradb_migration_test_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks - createDir(testDir) - var db = newLSMTree(testDir) - var ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE MIGRATION add_t { UP: CREATE TABLE t (id INTEGER); CREATE INDEX idx ON t(id); DOWN: DROP TABLE t; }")) - let dryRes = qexec.executeQuery(ctx, parse("MIGRATION DRYRUN add_t")) - check dryRes.success - check dryRes.message.contains("DRY RUN") - check dryRes.message.contains("Statements: 2") - check dryRes.message.contains("DOWN script: yes") - -suite "Parameterized queries": - var db: LSMTree - var ctx: qexec.ExecutionContext - - setup: - db = newLSMTree("") - ctx = qexec.newExecutionContext(db) - discard qexec.executeQuery(ctx, parse("CREATE TABLE users (id INT, name TEXT, age INT)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25)")) - - test "SELECT with placeholder params": - let sql = "SELECT * FROM users WHERE id = ?" - let tokens = lex.tokenize(sql) - let ast = parse(tokens) - let params = @[WireValue(kind: fkInt64, int64Val: 1)] - let r = qexec.executeQuery(ctx, ast, params) - check r.success - check r.rows.len == 1 - check r.rows[0]["name"] == "Alice" - - test "INSERT with placeholder params": - let sql = "INSERT INTO users (id, name, age) VALUES (?, ?, ?)" - let tokens = lex.tokenize(sql) - let ast = parse(tokens) - let params = @[ - WireValue(kind: fkInt64, int64Val: 3), - WireValue(kind: fkString, strVal: "Charlie"), - WireValue(kind: fkInt64, int64Val: 35) - ] - let r = qexec.executeQuery(ctx, ast, params) - check r.success - let selectR = qexec.executeQuery(ctx, parse("SELECT * FROM users WHERE id = 3")) - check selectR.rows.len == 1 - check selectR.rows[0]["name"] == "Charlie" - - test "SELECT with multiple placeholders": - let sql = "SELECT * FROM users WHERE age > ? AND name = ?" - let tokens = lex.tokenize(sql) - let ast = parse(tokens) - let params = @[WireValue(kind: fkInt64, int64Val: 25), WireValue(kind: fkString, strVal: "Alice")] - let r = qexec.executeQuery(ctx, ast, params) - check r.success - check r.rows.len == 1 - check r.rows[0]["name"] == "Alice" - - test "JSON type validation": - let createTbl = parse("CREATE TABLE json_test (id INT PRIMARY KEY, data JSON)") - discard qexec.executeQuery(ctx, createTbl) - let valid = parse("INSERT INTO json_test (id, data) VALUES (1, '{\"key\": \"value\"}')") - let r1 = qexec.executeQuery(ctx, valid) - check r1.success - let invalid = parse("INSERT INTO json_test (id, data) VALUES (2, 'not json')") - let r2 = qexec.executeQuery(ctx, invalid) - check not r2.success - check r2.message.contains("JSON") - - test "Multi-column index parse and create": - let ast = parse("CREATE INDEX idx_mc ON users (name, age)") - check ast.stmts[0].kind == nkCreateIndex - check ast.stmts[0].ciColumns.len == 2 - check ast.stmts[0].ciColumns[0] == "name" - check ast.stmts[0].ciColumns[1] == "age" - let r = qexec.executeQuery(ctx, ast) - check r.success - check r.message.contains("CREATE INDEX") - - test "CTE non-recursive execution": - let ast = parse("WITH active AS (SELECT * FROM users WHERE active = true) SELECT * FROM active") - let r = qexec.executeQuery(ctx, ast) - check r.success - check r.rows.len >= 1 - - test "CTE recursive parse": - let ast = parse("WITH RECURSIVE nums AS (SELECT 1 AS n) SELECT * FROM nums") - check ast.stmts[0].selWith.len == 1 - check ast.stmts[0].selWith[0][0] == "nums" - check ast.stmts[0].selWith[0][2] == true - - test "UNION ALL parse": - let ast = parse("SELECT 1 AS n UNION ALL SELECT 2 AS n") - check ast.stmts[0].kind == nkSetOp - check ast.stmts[0].setOpKind == sdkUnion - check ast.stmts[0].setOpAll == true - check ast.stmts[0].setOpLeft.kind == nkSelect - check ast.stmts[0].setOpRight.kind == nkSelect - - test "UNION ALL execution": - discard qexec.executeQuery(ctx, parse("INSERT INTO users (name, age, active) VALUES ('union_a', '30', 'true')")) - discard qexec.executeQuery(ctx, parse("INSERT INTO users (name, age, active) VALUES ('union_b', '25', 'false')")) - let ast = parse("SELECT name FROM users WHERE name = 'union_a' UNION ALL SELECT name FROM users WHERE name = 'union_b'") - let r = qexec.executeQuery(ctx, ast) - check r.success - check r.rows.len == 2 - - test "Simple recursive CTE execution": - let ast = parse("WITH RECURSIVE nums AS (SELECT 0 AS n FROM users LIMIT 1 UNION ALL SELECT n + 1 FROM nums WHERE n < 2) SELECT n FROM nums ORDER BY n ASC") - let r = qexec.executeQuery(ctx, ast) - check r.success - - test "DROP INDEX parse": - let ast = parse("DROP INDEX myidx") - check ast.stmts[0].kind == nkDropIndex - check ast.stmts[0].diName == "myidx" - - test "DROP INDEX execution": - let tbl = ctx.tables["users"] - let colKey = "users.name" - ctx.btrees[colKey] = newBTreeIndex[string, IndexEntry]() - let dropAst = parse("DROP INDEX users.name") - let r = qexec.executeQuery(ctx, dropAst) - check r.success - - test "JSON path operators parse": - let ast = parse("SELECT data->'name' FROM users") - check ast.stmts[0].kind == nkSelect - - test "JSON path operator ->> parse": - let ast = parse("SELECT data->>'name' FROM users") - check ast.stmts[0].kind == nkSelect - - test "JSON path execution": - discard qexec.executeQuery(ctx, parse("CREATE TABLE IF NOT EXISTS jsontest (id INT PRIMARY KEY, data JSON)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO jsontest (id, data) VALUES (1, '{\"name\": \"Alice\", \"age\": 30}')")) - let r = qexec.executeQuery(ctx, parse("SELECT data->'name' AS json_name, data->>'name' AS text_name FROM jsontest")) - check r.success - check r.rows.len >= 1 - - test "FTS match operator @@ parse": - let ast = parse("SELECT * FROM docs WHERE content @@ 'hello'") - check ast.stmts[0].kind == nkSelect - - test "FTS match operator @@ execution": - discard qexec.executeQuery(ctx, parse("INSERT INTO users (name, age, active) VALUES ('full text search', '30', 'true')")) - let r = qexec.executeQuery(ctx, parse("SELECT name FROM users WHERE name @@ 'text'")) - check r.success - # Should find the row because 'text' is in 'full text search' - - test "RECOVER TO TIMESTAMP parse": - let ast = parse("RECOVER TO TIMESTAMP '2026-05-07T12:00:00'") - check ast.stmts[0].kind == nkRecoverToTimestamp - - test "RECOVER FROM WAL execution": - let r = qexec.executeQuery(ctx, parse("RECOVER TO TIMESTAMP '2026-12-31T23:59:59'")) - check r.success - - test "FTS index creation USING FTS": - discard qexec.executeQuery(ctx, parse("CREATE TABLE IF NOT EXISTS fts_test (id INT PRIMARY KEY, body TEXT)")) - discard qexec.executeQuery(ctx, parse("INSERT INTO fts_test (id, body) VALUES (1, 'the quick brown fox jumps')")) - discard qexec.executeQuery(ctx, parse("INSERT INTO fts_test (id, body) VALUES (2, 'lazy dog sleeps all day')")) - discard qexec.executeQuery(ctx, parse("INSERT INTO fts_test (id, body) VALUES (3, 'quick brown dog plays fetch')")) - let r = qexec.executeQuery(ctx, parse("CREATE INDEX idx_fts_body ON fts_test(body) USING FTS")) - check r.success - check r.message.contains("USING FTS") - - test "FTS index @@ uses BM25": - let r = qexec.executeQuery(ctx, parse("SELECT id FROM fts_test WHERE body @@ 'quick brown'")) - check r.success - check r.rows.len >= 2 - -# JOIN tests -include "join_tests" - -# TLA+ faithfulness tests -include "tla_faithfulness" diff --git a/tests/test_lock b/tests/test_lock deleted file mode 100755 index 9824d92..0000000 Binary files a/tests/test_lock and /dev/null differ