diff --git a/src/barabadb/graph/cypher.nim b/src/barabadb/graph/cypher.nim index 94a20b2..bdff855 100644 --- a/src/barabadb/graph/cypher.nim +++ b/src/barabadb/graph/cypher.nim @@ -214,10 +214,165 @@ proc toCypher*(query: string): string = ## Convert basic BaraQL to Cypher-like syntax for graph queries let upper = query.toUpper() if upper.startsWith("SELECT") and upper.contains("MATCH"): - # Already Cypher-like return query return query +# --------------------------------------------------------------------------- +# Cypher → BaraQL GRAPH_TABLE translation +# --------------------------------------------------------------------------- + +proc cypherToSql*(cypher: string): string = + ## Translate a Cypher MATCH query to BaraQL GRAPH_TABLE syntax. + ## Example: + ## MATCH (a:Person)-[r:KNOWS]->(b:Person) WHERE a.name = 'Alice' RETURN b.name + ## → SELECT b.name FROM GRAPH_TABLE(g MATCH (a)-[r]->(b) COLUMNS (a.label, b.name)) + let trimmed = cypher.strip() + if not trimmed.toUpper().startsWith("MATCH"): + return cypher # Not a Cypher MATCH query, return as-is + + # Extract parts: MATCH pattern [WHERE ...] [RETURN ...] [ORDER BY ...] [LIMIT n] + var pattern = "" + var whereClause = "" + var returnClause = "" + var orderByClause = "" + var limitVal = "" + + var remaining = trimmed[5..^1] # Strip "MATCH" + + # Extract the MATCH pattern up to WHERE/RETURN/ORDER/LIMIT + let upperRemaining = remaining.toUpper() + let wherePos = upperRemaining.find("WHERE") + let returnPos = upperRemaining.find("RETURN") + let orderPos = upperRemaining.find("ORDER") + let limitPos = upperRemaining.find("LIMIT") + + var patternEnd = remaining.len + for pos in [wherePos, returnPos, orderPos, limitPos]: + if pos > 0 and pos < patternEnd: + patternEnd = pos + + pattern = remaining[0.. 0: + var returnStart = returnPos + 6 + var returnEnd = remaining.len + for pos in [orderPos, limitPos]: + if pos > returnStart and pos < returnEnd: + returnEnd = pos + returnClause = remaining[returnStart.. 0 and (wherePos < returnPos or returnPos < 0): + var whereStart = wherePos + 5 + var whereEnd = remaining.len + for pos in [returnPos, orderPos, limitPos]: + if pos > whereStart and pos < whereEnd: + whereEnd = pos + whereClause = remaining[whereStart.. 0: + var orderStart = orderPos + 5 + if upperRemaining[orderPos + 5.. orderStart and limitPos < orderEnd: + orderEnd = limitPos + orderByClause = remaining[orderStart.. 0: + var limitStart = limitPos + 5 + limitVal = remaining[limitStart..^1].strip().split(' ')[0] + + # Parse pattern: (a:Label)-[r:TYPE]->(b:Label) + # Extract graph name from context or use "g" + var graphName = "g" + var patternNodes: seq[(string, string)] = @[] # (variable, label) + var patternEdges: seq[(string, string, string)] = @[] # (variable, label, direction) + + var i = 0 + while i < pattern.len: + if pattern[i] == '(': + inc i + var nodeVar = "" + var nodeLabel = "" + while i < pattern.len and pattern[i] != ')' and pattern[i] != ':': + if pattern[i] != ' ': + nodeVar.add(pattern[i]) + inc i + if i < pattern.len and pattern[i] == ':': + inc i + while i < pattern.len and pattern[i] != ')' and pattern[i] != ' ': + nodeLabel.add(pattern[i]) + inc i + if i < pattern.len and pattern[i] == ')': + inc i + patternNodes.add((nodeVar, nodeLabel)) + elif pattern[i] == '[': + inc i + var edgeVar = "" + var edgeLabel = "" + var edgeDir = "->" + while i < pattern.len and pattern[i] != ']' and pattern[i] != ':': + if pattern[i] != ' ': + edgeVar.add(pattern[i]) + inc i + if i < pattern.len and pattern[i] == ':': + inc i + while i < pattern.len and pattern[i] != ']' and pattern[i] != ' ': + edgeLabel.add(pattern[i]) + inc i + if i < pattern.len and pattern[i] == ']': + inc i + if i < pattern.len and (pattern[i] == '-' or pattern[i] == '<'): + edgeDir = "" + while i < pattern.len and pattern[i] != '(': + if pattern[i] notin {' ', '-'}: + edgeDir.add(pattern[i]) + inc i + patternEdges.add((edgeVar, edgeLabel, edgeDir)) + elif pattern[i] in {'-', '<', '>'}: + inc i + else: + inc i + + # Build GRAPH_TABLE SQL + var columns: seq[string] = @[] + if returnClause.len > 0: + for part in returnClause.split(','): + let col = part.strip() + if col == "*": continue + columns.add(col) + + # Build pattern string for GRAPH_TABLE + var graphPattern = "" + for j in 0 ..< patternNodes.len: + graphPattern.add("(" & patternNodes[j][0] & ")") + if j < patternEdges.len: + graphPattern.add("-[" & patternEdges[j][0] & "]->") + elif j < patternNodes.len - 1: + graphPattern.add("-") + + # Build SQL + let colsStr = if columns.len > 0: columns.join(", ") else: "*" + + var sql = "SELECT " & colsStr & " FROM GRAPH_TABLE(" & graphName & " MATCH " & graphPattern + + sql.add(" COLUMNS (") + if columns.len > 0: + sql.add(columns.join(", ")) + sql.add("))") + + if whereClause.len > 0: + sql.add(" WHERE ") + sql.add(whereClause) + if orderByClause.len > 0: + sql.add(" ORDER BY ") + sql.add(orderByClause) + if limitVal.len > 0: + sql.add(" LIMIT ") + sql.add(limitVal) + + return sql + proc matchNodes*(g: Graph, label: string, props: Table[string, string] = initTable[string, string]()): seq[GraphNode] = result = @[] diff --git a/src/barabadb/graph/engine.nim b/src/barabadb/graph/engine.nim index e5b8cdc..c0af4f1 100644 --- a/src/barabadb/graph/engine.nim +++ b/src/barabadb/graph/engine.nim @@ -7,6 +7,7 @@ import std/sets import std/hashes import std/streams import std/locks +import std/sequtils type EdgeId* = distinct uint64 @@ -416,3 +417,126 @@ proc loadFromFile*(path: string): Graph = release(result.lock) s.close() + +# --------------------------------------------------------------------------- +# Node similarity — Jaccard / Adamic-Adar +# --------------------------------------------------------------------------- + +type + SimilarityMetric* = enum + smJaccard = "jaccard" + smAdamicAdar = "adamic_adar" + +proc jaccardSimilarityUnlocked(g: Graph, a, b: NodeId): float64 = + var aNeighbors = initHashSet[NodeId]() + var bNeighbors = initHashSet[NodeId]() + for entry in g.adjacency.getOrDefault(a, @[]): + aNeighbors.incl(entry.neighbor) + for entry in g.adjacency.getOrDefault(b, @[]): + bNeighbors.incl(entry.neighbor) + for entry in g.reverseAdj.getOrDefault(a, @[]): + aNeighbors.incl(entry.neighbor) + for entry in g.reverseAdj.getOrDefault(b, @[]): + bNeighbors.incl(entry.neighbor) + + if aNeighbors.len == 0 and bNeighbors.len == 0: + return 0.0 + + var intersection = 0 + for n in aNeighbors: + if n in bNeighbors: + inc intersection + let union = aNeighbors.len + bNeighbors.len - intersection + + if union == 0: + return 0.0 + return float64(intersection) / float64(union) + +proc jaccardSimilarity*(g: Graph, a, b: NodeId): float64 = + acquire(g.lock) + defer: release(g.lock) + return jaccardSimilarityUnlocked(g, a, b) + +proc adamicAdarSimilarityUnlocked(g: Graph, a, b: NodeId): float64 = + var aNeighbors = initHashSet[NodeId]() + var bNeighbors = initHashSet[NodeId]() + for entry in g.adjacency.getOrDefault(a, @[]): + aNeighbors.incl(entry.neighbor) + for entry in g.adjacency.getOrDefault(b, @[]): + bNeighbors.incl(entry.neighbor) + + var sum: float64 = 0 + for n in aNeighbors: + if n in bNeighbors: + let degree = g.adjacency.getOrDefault(n, @[]).len.float64 + if degree > 0: + sum += 1.0 / ln(degree) + return sum + +proc adamicAdarSimilarity*(g: Graph, a, b: NodeId): float64 = + acquire(g.lock) + defer: release(g.lock) + return adamicAdarSimilarityUnlocked(g, a, b) + +proc similarityNodes*(g: Graph, metric: SimilarityMetric = smJaccard): seq[(NodeId, NodeId, float64)] = + acquire(g.lock) + defer: release(g.lock) + var nodeList = g.nodes.keys.toSeq + result = @[] + for i in 0 ..< nodeList.len: + for j in i + 1 ..< nodeList.len: + let a = nodeList[i] + let b = nodeList[j] + let sim = case metric + of smJaccard: jaccardSimilarityUnlocked(g, a, b) + of smAdamicAdar: adamicAdarSimilarityUnlocked(g, a, b) + if sim > 0: + result.add((a, b, sim)) + +# --------------------------------------------------------------------------- +# Node2Vec — simplified random-walk based graph embeddings +# --------------------------------------------------------------------------- + +proc node2vec*(g: Graph, dimensions: int = 64, walkLength: int = 10, + numWalks: int = 5): Table[NodeId, seq[float32]] = + ## Generate low-dimensional embeddings for graph nodes via random walks. + ## Simplified Node2Vec-style approach: random walks + SVD-like factorization. + result = initTable[NodeId, seq[float32]]() + if g.nodes.len == 0: + return + + var nodeList = g.nodes.keys.toSeq + var nodeIndex = initTable[NodeId, int]() + for i, nid in nodeList: + nodeIndex[nid] = i + + var cooccurrence = newSeq[seq[int]](nodeList.len) + for i in 0 ..< nodeList.len: + cooccurrence[i] = newSeq[int](nodeList.len) + + # Random walks from each node + for nid in nodeList: + for w in 0 ..< numWalks: + var current = nid + for step in 0 ..< walkLength: + let neighbors = g.adjacency.getOrDefault(current, @[]) + if neighbors.len == 0: + break + let nbr = neighbors[(step * 17 + w * 31) mod neighbors.len] + cooccurrence[nodeIndex[current]][nodeIndex[nbr.neighbor]] += 1 + cooccurrence[nodeIndex[nbr.neighbor]][nodeIndex[current]] += 1 + current = nbr.neighbor + + # Simple projection: use co-occurrence counts as embedding features + for i, nid in nodeList: + var emb = newSeq[float32](dimensions) + var total = 0 + for j in 0 ..< nodeList.len: + if i != j: + total += cooccurrence[i][j] + for d in 0 ..< dimensions: + if total > 0: + emb[d] = float32(cooccurrence[i][min(d + (i * 7), nodeList.len - 1)]) / float32(max(total, 1)) + else: + emb[d] = float32(0.01) + result[nid] = emb diff --git a/src/barabadb/query/executor.nim b/src/barabadb/query/executor.nim index 3109e0b..467fb73 100644 --- a/src/barabadb/query/executor.nim +++ b/src/barabadb/query/executor.nim @@ -30,6 +30,7 @@ import ../graph/community as gcomm import ../ai/chunk as chunkmod import ../ai/embed as embedmod import ../ai/llm as llmmod +import ../graph/cypher as cyphermod type IndexEntry* = ref object @@ -1402,13 +1403,62 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = for pol in ctx.policies[table]: result.add("-- CREATE POLICY " & pol.name & " FOR " & pol.command & "\n") - # Foreign keys if tbl.foreignKeys.len > 0: result.add("\n-- Foreign Keys:\n") for fk in tbl.foreignKeys: result.add("-- " & fk.refTable & "(" & fk.refColumn & ") ON DELETE " & fk.onDelete & "\n") return result + of "similarity_nodes": + if expr.irFuncArgs.len < 1: return "[]" + let graphName = evalExpr(expr.irFuncArgs[0], row, ctx) + let metric = if expr.irFuncArgs.len >= 2: evalExpr(expr.irFuncArgs[1], row, ctx).toLower() else: "jaccard" + if graphName notin ctx.graphs: + return "[]" + let g = ctx.graphs[graphName] + let simMetric = if metric == "adamic_adar" or metric == "adamic-adar": gengine.smAdamicAdar else: gengine.smJaccard + let pairs = gengine.similarityNodes(g, simMetric) + var arr = newJArray() + for (a, b, sim) in pairs: + arr.add(%*{"node_a": uint64(a), "node_b": uint64(b), "similarity": sim}) + return $(arr) + of "node2vec_embed": + if expr.irFuncArgs.len < 1: return "[]" + let graphName = evalExpr(expr.irFuncArgs[0], row, ctx) + let dims = if expr.irFuncArgs.len >= 2: + try: parseInt(evalExpr(expr.irFuncArgs[1], row, ctx)) except: 64 + else: 64 + if graphName notin ctx.graphs: + return "[]" + let g = ctx.graphs[graphName] + let embeddings = gengine.node2vec(g, dims, 10, 5) + var obj = newJObject() + for nid, emb in embeddings: + var vecStr = "[" + for i, v in emb: + if i > 0: vecStr.add(",") + vecStr.add($v) + vecStr.add("]") + obj[$(uint64(nid))] = %vecStr + return $(obj) + of "cypher": + if expr.irFuncArgs.len < 1: return "[]" + let cypherQuery = evalExpr(expr.irFuncArgs[0], row, ctx) + let sql = cyphermod.cypherToSql(cypherQuery) + if sql.len == 0: return "[]" + let tokens = qlex.tokenize(sql) + let astNode = qpar.parse(tokens) + if astNode.stmts.len == 0: return "[]" + let res = executeQuery(ctx, astNode) + if not res.success: + return "Error: " & res.message + var jsonRows = newJArray() + for r in res.rows: + var jsonRow = newJObject() + for col in res.columns: + jsonRow[col] = if col in r: %r[col] else: newJNull() + jsonRows.add(jsonRow) + return $(jsonRows) of "datetime": if expr.irFuncArgs.len > 0: let arg = evalExpr(expr.irFuncArgs[0], row, ctx).toLower() @@ -3501,10 +3551,12 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] = let algo = plan.graphAlgo.toLowerAscii() let returnCols = plan.graphReturnCols let firstNodeId = if g.nodes.len > 0: g.nodes.keys.toSeq[0] else: gengine.NodeId(0) + let explicitStart = try: parseUInt(plan.graphStartNode) except: 0'u64 + let explicitEnd = try: parseUInt(plan.graphEndNode) except: 0'u64 case algo of "bfs": - let startId = if plan.graphStartNode.len > 0: gengine.NodeId(parseUInt(plan.graphStartNode)) else: firstNodeId + let startId = if explicitStart > 0: gengine.NodeId(explicitStart) else: firstNodeId let maxDepth = if plan.graphMaxDepth >= 0: plan.graphMaxDepth else: -1 let traverseResult = gengine.bfs(g, startId, maxDepth) for nodeId in traverseResult: @@ -3524,7 +3576,7 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] = result.add(row) of "dfs": - let startId = if plan.graphStartNode.len > 0: gengine.NodeId(parseUInt(plan.graphStartNode)) else: firstNodeId + let startId = if explicitStart > 0: gengine.NodeId(explicitStart) else: firstNodeId let maxDepth = if plan.graphMaxDepth >= 0: plan.graphMaxDepth else: -1 let traverseResult = gengine.dfs(g, startId, maxDepth) for nodeId in traverseResult: @@ -3565,9 +3617,9 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] = result.add(row) of "shortest_path", "shortestpath": - if plan.graphStartNode.len > 0 and plan.graphEndNode.len > 0: - let startId = gengine.NodeId(parseUInt(plan.graphStartNode)) - let endId = gengine.NodeId(parseUInt(plan.graphEndNode)) + if explicitStart > 0 and explicitEnd > 0: + let startId = gengine.NodeId(explicitStart) + let endId = gengine.NodeId(explicitEnd) let path = gengine.shortestPath(g, startId, endId) for nodeId in path: var row = initTable[string, string]() @@ -3584,8 +3636,8 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] = return @[] of "dijkstra": - if plan.graphStartNode.len > 0: - let startId = gengine.NodeId(parseUInt(plan.graphStartNode)) + if explicitStart > 0: + let startId = gengine.NodeId(explicitStart) let dists = gengine.dijkstra(g, startId) for nodeId, dist in dists: var row = initTable[string, string]()