diff --git a/examples/tutorial.nim b/examples/tutorial.nim new file mode 100644 index 0000000..7ae3e59 --- /dev/null +++ b/examples/tutorial.nim @@ -0,0 +1,232 @@ +## BaraDB Examples — getting started tutorials +import ../src/barabadb/storage/lsm +import ../src/barabadb/storage/btree +import ../src/barabadb/vector/engine +import ../src/barabadb/graph/engine +import ../src/barabadb/graph/community +import ../src/barabadb/fts/engine +import ../src/barabadb/core/mvcc +import ../src/barabadb/query/lexer +import ../src/barabadb/query/parser + +# Example 1: Basic Key-Value Store +proc exampleKeyValue() = + echo "=== Example 1: Key-Value Store ===" + var db = newLSMTree("./tutorial_kv") + + # Put data + db.put("user:1", cast[seq[byte]]("Alice,30,Engineer")) + db.put("user:2", cast[seq[byte]]("Bob,25,Designer")) + db.put("user:3", cast[seq[byte]]("Charlie,35,Manager")) + + # Get data + let (found, value) = db.get("user:1") + if found: + echo " user:1 = ", cast[string](value) + + # Contains check + echo " user:1 exists: ", db.contains("user:1") + echo " user:99 exists: ", db.contains("user:99") + + # Delete + db.delete("user:3") + echo " user:3 deleted: ", db.contains("user:3") + + db.close() + +# Example 2: B-Tree Range Queries +proc exampleBTree() = + echo "=== Example 2: B-Tree Range Queries ===" + var btree = newBTreeIndex[string, string]() + + for i in 0..<100: + btree.insert("key_" & $i, "value_" & $i) + + # Point query + let vals = btree.get("key_42") + echo " Point query key_42: ", vals + + # Range scan + let range = btree.scan("key_10", "key_20") + echo " Range scan key_10..key_20: ", range.len, " results" + echo " First: ", range[0][0], " = ", range[0][1] + echo " Last: ", range[^1][0], " = ", range[^1][1] + +# Example 3: Vector Similarity Search +proc exampleVectorSearch() = + echo "=== Example 3: Vector Similarity Search ===" + var idx = newHNSWIndex(dimensions = 3) + + # Insert vectors with metadata + idx.insert(1, @[1.0'f32, 0.0'f32, 0.0'f32], {"type": "red"}.toTable) + idx.insert(2, @[0.0'f32, 1.0'f32, 0.0'f32], {"type": "green"}.toTable) + idx.insert(3, @[0.0'f32, 0.0'f32, 1.0'f32], {"type": "blue"}.toTable) + idx.insert(4, @[0.9'f32, 0.1'f32, 0.0'f32], {"type": "red"}.toTable) + + # Search similar vectors + let query = @[1.0'f32, 0.0'f32, 0.0'f32] + let results = idx.search(query, k = 3) + echo " Search results for [1.0, 0.0, 0.0]:" + for (id, dist) in results: + echo " ID: ", id, " distance: ", dist + +# Example 4: Graph Traversal +proc exampleGraph() = + echo "=== Example 4: Graph Traversal ===" + var g = newGraph() + + # Create nodes + let alice = g.addNode("Person", {"name": "Alice", "age": "30"}.toTable) + let bob = g.addNode("Person", {"name": "Bob", "age": "25"}.toTable) + let charlie = g.addNode("Person", {"name": "Charlie", "age": "35"}.toTable) + let diana = g.addNode("Person", {"name": "Diana", "age": "28"}.toTable) + + # Create edges + discard g.addEdge(alice, bob, "KNOWS") + discard g.addEdge(bob, charlie, "KNOWS") + discard g.addEdge(alice, diana, "KNOWS") + + # BFS traversal + let bfs = g.bfs(alice) + echo " BFS from Alice: ", bfs.len, " nodes" + + # Shortest path + let path = g.shortestPath(alice, charlie) + echo " Shortest path Alice→Charlie: ", path.len, " hops" + + # PageRank + let ranks = g.pageRank() + echo " PageRank top node: ", ranks.entries().sorted( + proc(a, b: (NodeId, float64)): int = cmp(b[1], a[1]))[0] + +# Example 5: Full-Text Search +proc exampleFTS() = + echo "=== Example 5: Full-Text Search ===" + var idx = newInvertedIndex() + + # Add documents + idx.addDocument(1, "Nim is a statically typed compiled language with Python-like syntax") + idx.addDocument(2, "Python is an interpreted language popular for data science") + idx.addDocument(3, "Rust is a systems programming language with memory safety") + idx.addDocument(4, "JavaScript runs in browsers and on servers via Node.js") + + # BM25 search + echo " BM25 search: 'programming language':" + for result in idx.search("programming language", limit = 3): + echo " Doc ", result.docId, " score: ", result.score + + # TF-IDF search + echo " TF-IDF search: 'compiled language':" + for result in idx.searchTfidf("compiled language", limit = 3): + echo " Doc ", result.docId, " score: ", result.score + + # Fuzzy search + echo " Fuzzy search: 'propgramming' (typo):" + for result in idx.fuzzySearch("propgramming", maxDistance = 2, limit = 3): + echo " Doc ", result.docId, " score: ", result.score + +# Example 6: Transactions (MVCC) +proc exampleTransactions() = + echo "=== Example 6: Transactions ===" + var tm = newTxnManager() + + # Transaction 1: write + let txn1 = tm.beginTxn() + discard tm.write(txn1, "balance:alice", cast[seq[byte]]("100")) + discard tm.write(txn1, "balance:bob", cast[seq[byte]]("200")) + discard tm.commit(txn1) + + # Transaction 2: snapshot isolation + let txn2 = tm.beginTxn() + # Can't see concurrent writes + let (found, val) = tm.read(txn2, "balance:alice") + echo " Alice balance (txn2 before commit): ", if found: cast[string](val) else: "nil" + + # Transaction 3: concurrent write + let txn3 = tm.beginTxn() + discard tm.write(txn3, "balance:alice", cast[seq[byte]]("150")) + discard tm.commit(txn3) + + # txn2 still sees old value (snapshot) + let (found2, val2) = tm.read(txn2, "balance:alice") + echo " Alice balance (txn2 after txn3 commit): ", if found2: cast[string](val2) else: "nil" + discard tm.commit(txn2) + + # New transaction sees latest + let txn4 = tm.beginTxn() + let (found3, val3) = tm.read(txn4, "balance:alice") + echo " Alice balance (new txn): ", if found3: cast[string](val3) else: "nil" + discard tm.commit(txn4) + +# Example 7: BaraQL Query Parsing +proc exampleBaraQL() = + echo "=== Example 7: BaraQL Query Parsing ===" + + let queries = [ + "SELECT name, age FROM users WHERE age > 18 ORDER BY name LIMIT 10", + "INSERT users { name := 'Alice', age := 30 }", + "UPDATE users SET name = 'Bob' WHERE id = 1", + "DELETE FROM users WHERE id = 99", + "SELECT dept, count(*), avg(salary) FROM employees GROUP BY dept HAVING count(*) > 5", + "SELECT u.name, o.total FROM users u LEFT JOIN orders o ON u.id = o.user_id", + "WITH recent AS (SELECT * FROM orders WHERE date > '2025-01-01') SELECT * FROM recent", + ] + + for query in queries: + try: + let tokens = tokenize(query) + let ast = parse(tokens) + echo " ✓ ", query[0..= 2: # Only rebalance with 2+ nodes + cm.router.rebalance(cm.nodes) + +proc removeNode*(cm: ClusterMembership, nodeId: string) = + var newNodes: seq[string] = @[] + for n in cm.nodes: + if n != nodeId: + newNodes.add(n) + cm.nodes = newNodes + if cm.nodes.len >= 1: + cm.router.rebalance(cm.nodes) + +proc onNodeJoin*(cm: ClusterMembership, nodeId: string) = + echo "[cluster] node joined: ", nodeId + cm.addNode(nodeId) + +proc onNodeLeave*(cm: ClusterMembership, nodeId: string) = + echo "[cluster] node left: ", nodeId + cm.removeNode(nodeId) + +proc onNodeFail*(cm: ClusterMembership, nodeId: string) = + echo "[cluster] node failed: ", nodeId + cm.removeNode(nodeId) + # Re-assign shards that were on the failed node + for i in 0..", "<-", "--" + properties*: Table[string, string] + + CypherPattern* = object + nodes*: seq[CypherNode] + edges*: seq[CypherEdge] + + CypherQuery* = object + kind*: string # "MATCH", "CREATE", "MERGE", "DELETE" + pattern*: CypherPattern + whereClause*: string + returnExprs*: seq[string] + orderBy*: string + limit*: int + + CypherResult* = object + columns*: seq[string] + rows*: seq[seq[string]] + +proc parseCypher*(query: string): CypherQuery = + result = CypherQuery(returnExprs: @[], limit: 0) + let upper = query.toUpper().strip() + + if upper.startsWith("MATCH"): + result.kind = "MATCH" + elif upper.startsWith("CREATE"): + result.kind = "CREATE" + elif upper.startsWith("MERGE"): + result.kind = "MERGE" + else: + return + + # Parse node pattern: (variable:Label {props}) + var pos = 0 + var nodes: seq[CypherNode] = @[] + var edges: seq[CypherEdge] = @[] + + while pos < query.len: + if query[pos] == '(': + # Parse node + inc pos + var variable = "" + var label = "" + var props = initTable[string, string]() + + # Read variable name + while pos < query.len and query[pos] notin {':', ' ', '{', ')'}: + variable &= query[pos] + inc pos + + if pos < query.len and query[pos] == ':': + inc pos + while pos < query.len and query[pos] notin {' ', '{', ')'}: + label &= query[pos] + inc pos + + if pos < query.len and query[pos] == '{': + inc pos + var key = "" + var value = "" + var inKey = true + while pos < query.len and query[pos] != '}': + let ch = query[pos] + if ch == ':': + inKey = false + elif ch in {',', ' '}: + if key.len > 0 and value.len > 0: + props[key.strip()] = value.strip().strip(chars = {'"'}) + key = "" + value = "" + inKey = true + elif inKey: + key &= ch + else: + value &= ch + inc pos + if key.len > 0 and value.len > 0: + props[key.strip()] = value.strip().strip(chars = {'"'}) + inc pos # skip } + + nodes.add(CypherNode(variable: variable, label: label, properties: props)) + inc pos # skip ) + elif query[pos] == '-' or query[pos] == '<' or query[pos] == '[': + # Parse edge + var direction = "" + var edgeVar = "" + var edgeLabel = "" + + if query[pos] == '<': + inc pos + direction = "<-" + + if query[pos] == '-': + inc pos + if direction == "": + direction = "-" + else: + direction &= "-" + + if pos < query.len and query[pos] == '[': + inc pos + while pos < query.len and query[pos] notin {']', ':'}: + edgeVar &= query[pos] + inc pos + if pos < query.len and query[pos] == ':': + inc pos + while pos < query.len and query[pos] != ']': + edgeLabel &= query[pos] + inc pos + inc pos # skip ] + + if pos < query.len and query[pos] == '-': + inc pos + direction &= "-" + if pos < query.len and query[pos] == '>': + inc pos + direction &= ">" + + edges.add(CypherEdge(variable: edgeVar, label: edgeLabel, direction: direction)) + else: + inc pos + + result.pattern.nodes = nodes + result.pattern.edges = edges + + # Parse WHERE + let wherePos = query.toUpper().find(" WHERE ") + if wherePos >= 0: + let whereStart = wherePos + 7 + let returnPos = query.toUpper().find(" RETURN ") + if returnPos > wherePos: + result.whereClause = query[whereStart..= 0: + let returnContent = query[returnPos + 8..^1] + for expr in returnContent.split(","): + let trimmed = expr.strip() + if trimmed.len > 0: + result.returnExprs.add(trimmed) + + # Parse ORDER BY + let orderPos = query.toUpper().find(" ORDER BY ") + if orderPos >= 0: + let limPos = query.toUpper().find(" LIMIT ") + if limPos > orderPos: + result.orderBy = query[orderPos + 9..= 0: + try: + result.limit = parseInt(query[limPos + 7..^1].strip()) + except: + result.limit = 0 + +proc executeCypher*(g: Graph, query: CypherQuery): CypherResult = + result = CypherResult(columns: @[], rows: @[]) + + if query.pattern.nodes.len == 0: + return + + # Basic MATCH execution + if query.kind == "MATCH": + # For each node matching the pattern, collect results + for nodeId, node in g.nodes: + let patternNode = query.pattern.nodes[0] + if patternNode.label.len == 0 or node.label == patternNode.label: + var propsMatch = true + for pk, pv in patternNode.properties: + if node.properties.getOrDefault(pk, "") != pv: + propsMatch = false + break + if propsMatch: + var row: seq[string] = @[] + for expr in query.returnExprs: + if expr == patternNode.variable: + row.add(node.label) + elif expr.startsWith(patternNode.variable & "."): + let propName = expr[expr.find('.') + 1 .. ^1] + row.add(node.properties.getOrDefault(propName, "")) + else: + row.add(expr) + result.rows.add(row) + + result.columns = query.returnExprs + if query.limit > 0 and result.rows.len > query.limit: + result.rows = result.rows[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"