Files
Baradb/src/barabadb/graph/cypher.nim
T
dimgigov a0c5ce8598 feat: Session 11 finale — similarity_nodes, node2vec, Cypher layer
- similarity_nodes(graph, metric): Jaccard / Adamic-Adar similarity between all node pairs
- node2vec_embed(graph, dimensions): Random-walk based graph embeddings
- cypher(query): Translate Cypher MATCH...RETURN → BaraQL GRAPH_TABLE SQL
- Fixed recursive lock deadlock in similarity functions
- Fixed graph start node parsing (pattern variable vs numeric ID)
- Added sequtils import to graph engine
- All 340+ existing tests pass
2026-05-17 16:00:15 +03:00

388 lines
12 KiB
Nim

## Cypher-like Graph Query Extension for BaraQL
import std/tables
import std/strutils
import std/locks
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
acquire(g.lock)
defer: release(g.lock)
# 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
let upper = query.toUpper()
if upper.startsWith("SELECT") and upper.contains("MATCH"):
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..<patternEnd].strip()
if returnPos > 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..<returnEnd].strip()
if wherePos > 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..<whereEnd].strip()
if orderPos > 0:
var orderStart = orderPos + 5
if upperRemaining[orderPos + 5..<orderPos + 7] == "BY":
orderStart = orderPos + 8
var orderEnd = remaining.len
if limitPos > orderStart and limitPos < orderEnd:
orderEnd = limitPos
orderByClause = remaining[orderStart..<orderEnd].strip()
if limitPos > 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 = @[]
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)