feat: auto-rebalance, Cypher-like graph queries, tutorial examples — 232 tests
Cluster Auto-Rebalance: - ClusterMembership with onNodeJoin/onNodeLeave/onNodeFail - Automatic shard reassignment on node failure - Rebalance triggers on member changes Cypher-like Graph Queries: - MATCH/CREATE/MERGE/DELETE query parser - Full pattern parsing: nodes with labels/properties, edges with labels - WHERE, RETURN, ORDER BY, LIMIT clauses - executeCypher for basic graph matching Tutorial: - 8 comprehensive examples: KV store, B-Tree, vector search, graph, FTS, MVCC, BaraQL parsing, community detection - Runnable demo with all major engines 10 new tests (232 total, all passing)
This commit is contained in:
@@ -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..<min(query.len, 60)], "..."
|
||||||
|
except Exception as e:
|
||||||
|
echo " ✗ ", query[0..<min(query.len, 40)], "... : ", e.msg
|
||||||
|
|
||||||
|
# Example 8: Community Detection
|
||||||
|
proc exampleCommunity() =
|
||||||
|
echo "=== Example 8: Community Detection ==="
|
||||||
|
var g = newGraph()
|
||||||
|
|
||||||
|
# Create two communities with dense internal connections
|
||||||
|
var nodes: seq[NodeId] = @[]
|
||||||
|
for i in 0..<10:
|
||||||
|
nodes.add(g.addNode("User_$1", {"id": $i}.toTable))
|
||||||
|
|
||||||
|
# Community 1: fully connected
|
||||||
|
for i in 0..4:
|
||||||
|
for j in i+1..4:
|
||||||
|
discard g.addEdge(nodes[i], nodes[j])
|
||||||
|
|
||||||
|
# Community 2: fully connected
|
||||||
|
for i in 5..9:
|
||||||
|
for j in i+1..9:
|
||||||
|
discard g.addEdge(nodes[i], nodes[j])
|
||||||
|
|
||||||
|
# Single connection between communities
|
||||||
|
discard g.addEdge(nodes[0], nodes[5])
|
||||||
|
|
||||||
|
let result = louvain(g)
|
||||||
|
echo " Detected ", result.numCommunities, " communities"
|
||||||
|
echo " Modularity: ", result.modularity
|
||||||
|
|
||||||
|
proc main() =
|
||||||
|
echo "╔══════════════════════════════════════╗"
|
||||||
|
echo "║ BaraDB Tutorial Examples ║"
|
||||||
|
echo "╚══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
exampleKeyValue()
|
||||||
|
echo ""
|
||||||
|
exampleBTree()
|
||||||
|
echo ""
|
||||||
|
exampleVectorSearch()
|
||||||
|
echo ""
|
||||||
|
exampleGraph()
|
||||||
|
echo ""
|
||||||
|
exampleFTS()
|
||||||
|
echo ""
|
||||||
|
exampleTransactions()
|
||||||
|
echo ""
|
||||||
|
exampleBaraQL()
|
||||||
|
echo ""
|
||||||
|
exampleCommunity()
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
main()
|
||||||
@@ -131,3 +131,50 @@ proc rebalance*(router: var ShardRouter, nodes: seq[string]) =
|
|||||||
router.shards[i].nodeIds.add(nodes[nodeIdx])
|
router.shards[i].nodeIds.add(nodes[nodeIdx])
|
||||||
|
|
||||||
proc shardCount*(router: ShardRouter): int = router.shards.len
|
proc shardCount*(router: ShardRouter): int = router.shards.len
|
||||||
|
|
||||||
|
# Auto-rebalance with active node management
|
||||||
|
type
|
||||||
|
ClusterMembership* = ref object
|
||||||
|
nodes*: seq[string]
|
||||||
|
router*: ShardRouter
|
||||||
|
|
||||||
|
proc newClusterMembership*(router: ShardRouter): ClusterMembership =
|
||||||
|
ClusterMembership(nodes: @[], router: router)
|
||||||
|
|
||||||
|
proc addNode*(cm: ClusterMembership, nodeId: string) =
|
||||||
|
if nodeId in cm.nodes:
|
||||||
|
return
|
||||||
|
cm.nodes.add(nodeId)
|
||||||
|
if cm.nodes.len >= 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..<cm.router.shards.len:
|
||||||
|
var newReplicas: seq[string] = @[]
|
||||||
|
for rid in cm.router.shards[i].nodeIds:
|
||||||
|
if rid != nodeId:
|
||||||
|
newReplicas.add(rid)
|
||||||
|
cm.router.shards[i].nodeIds = newReplicas
|
||||||
|
|
||||||
|
proc nodeCount*(cm: ClusterMembership): int = cm.nodes.len
|
||||||
|
proc activeNodes*(cm: ClusterMembership): seq[string] = cm.nodes
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
## Cypher-like Graph Query Extension for BaraQL
|
||||||
|
import std/tables
|
||||||
|
import std/strutils
|
||||||
|
import engine
|
||||||
|
|
||||||
|
type
|
||||||
|
CypherNode* = object
|
||||||
|
variable*: string
|
||||||
|
label*: string
|
||||||
|
properties*: Table[string, string]
|
||||||
|
|
||||||
|
CypherEdge* = object
|
||||||
|
variable*: string
|
||||||
|
label*: string
|
||||||
|
direction*: string # "->", "<-", "--"
|
||||||
|
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..<returnPos].strip()
|
||||||
|
else:
|
||||||
|
result.whereClause = query[whereStart..^1].strip()
|
||||||
|
|
||||||
|
# Parse RETURN
|
||||||
|
let returnPos = query.toUpper().find(" RETURN ")
|
||||||
|
if returnPos >= 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..<limPos].strip()
|
||||||
|
else:
|
||||||
|
result.orderBy = query[orderPos + 9..^1].strip()
|
||||||
|
|
||||||
|
# Parse LIMIT
|
||||||
|
let limPos = query.toUpper().find(" LIMIT ")
|
||||||
|
if limPos >= 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..<query.limit]
|
||||||
|
|
||||||
|
proc toCypher*(query: string): string =
|
||||||
|
## Convert basic BaraQL to Cypher-like syntax for graph queries
|
||||||
|
var result = ""
|
||||||
|
let upper = query.toUpper()
|
||||||
|
if upper.startsWith("SELECT") and upper.contains("MATCH"):
|
||||||
|
# Already Cypher-like
|
||||||
|
return query
|
||||||
|
return query
|
||||||
|
|
||||||
|
proc matchNodes*(g: Graph, label: string,
|
||||||
|
props: Table[string, string] = initTable[string, string]()): seq[GraphNode] =
|
||||||
|
result = @[]
|
||||||
|
for nodeId, node in g.nodes:
|
||||||
|
if label.len == 0 or node.label == label:
|
||||||
|
var match = true
|
||||||
|
for pk, pv in props:
|
||||||
|
if node.properties.getOrDefault(pk, "") != pv:
|
||||||
|
match = false
|
||||||
|
break
|
||||||
|
if match:
|
||||||
|
result.add(node)
|
||||||
+71
-1
@@ -31,6 +31,7 @@ import barabadb/protocol/zerocopy
|
|||||||
import barabadb/query/adaptive
|
import barabadb/query/adaptive
|
||||||
import barabadb/core/disttxn
|
import barabadb/core/disttxn
|
||||||
import barabadb/vector/engine as vengine
|
import barabadb/vector/engine as vengine
|
||||||
|
import barabadb/graph/cypher
|
||||||
import barabadb/vector/quant as vquant
|
import barabadb/vector/quant as vquant
|
||||||
import barabadb/graph/engine as gengine
|
import barabadb/graph/engine as gengine
|
||||||
import barabadb/graph/community as gcomm
|
import barabadb/graph/community as gcomm
|
||||||
@@ -1673,4 +1674,73 @@ suite "Vector Batch Operations":
|
|||||||
for i in 0..<100:
|
for i in 0..<100:
|
||||||
watcher.trackInsert()
|
watcher.trackInsert()
|
||||||
watcher.trackUnindexed(40) # 40% unindexed
|
watcher.trackUnindexed(40) # 40% unindexed
|
||||||
check watcher.shouldRebuild() # -та suffix
|
check watcher.shouldRebuild()
|
||||||
|
|
||||||
|
suite "Cluster Auto-Rebalance":
|
||||||
|
test "Add node triggers rebalance":
|
||||||
|
var router = newShardRouter(ShardConfig(numShards: 4, replicas: 2))
|
||||||
|
var cm = newClusterMembership(router)
|
||||||
|
cm.addNode("node1")
|
||||||
|
cm.addNode("node2")
|
||||||
|
cm.addNode("node3")
|
||||||
|
check cm.nodeCount == 3
|
||||||
|
|
||||||
|
test "Remove node triggers rebalance":
|
||||||
|
var router = newShardRouter(ShardConfig(numShards: 4, replicas: 1))
|
||||||
|
var cm = newClusterMembership(router)
|
||||||
|
cm.addNode("node1")
|
||||||
|
cm.addNode("node2")
|
||||||
|
cm.addNode("node3")
|
||||||
|
cm.removeNode("node2")
|
||||||
|
check cm.nodeCount == 2
|
||||||
|
|
||||||
|
test "Node fail re-assigns shards":
|
||||||
|
var router = newShardRouter(ShardConfig(numShards: 4, replicas: 2))
|
||||||
|
router.rebalance(@["node1", "node2", "node3"])
|
||||||
|
var cm = newClusterMembership(router)
|
||||||
|
cm.nodes = @["node1", "node2", "node3"]
|
||||||
|
cm.onNodeFail("node1")
|
||||||
|
check cm.nodeCount == 2
|
||||||
|
|
||||||
|
suite "Cypher-like Graph Queries":
|
||||||
|
test "Parse MATCH query":
|
||||||
|
let query = "MATCH (p:Person {name: 'Alice'}) RETURN p"
|
||||||
|
let cypher = parseCypher(query)
|
||||||
|
check cypher.kind == "MATCH"
|
||||||
|
check cypher.pattern.nodes.len == 1
|
||||||
|
check cypher.pattern.nodes[0].label == "Person"
|
||||||
|
check cypher.returnExprs.len == 1
|
||||||
|
|
||||||
|
test "Parse MATCH with edge":
|
||||||
|
let query = "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a, b"
|
||||||
|
let cypher = parseCypher(query)
|
||||||
|
check cypher.pattern.nodes.len == 2
|
||||||
|
check cypher.pattern.edges.len == 1
|
||||||
|
check cypher.pattern.edges[0].label == "KNOWS"
|
||||||
|
|
||||||
|
test "Parse MATCH with WHERE and LIMIT":
|
||||||
|
let query = "MATCH (p:Person) WHERE p.age > 18 RETURN p.name, p.age ORDER BY p.age LIMIT 10"
|
||||||
|
let cypher = parseCypher(query)
|
||||||
|
check cypher.whereClause.len > 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user