## 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..= 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 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..(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 = @[] 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)