Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/graphforge/ast/clause.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,12 @@ class MergeClause:

Examples:
MERGE (n:Person {name: 'Alice'})
MERGE (n:Person {id: 1}) ON CREATE SET n.created = timestamp()
MERGE (a)-[r:KNOWS]->(b)
"""

patterns: list[Any] # List of NodePattern or RelationshipPattern
on_create: "SetClause | None" = None # Optional ON CREATE SET clause


@dataclass
Expand Down
64 changes: 40 additions & 24 deletions src/graphforge/executor/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,28 +1009,7 @@ def _execute_set(self, op: Set, input_rows: list[ExecutionContext]) -> list[Exec
result = []

for ctx in input_rows:
# Process each SET item
for property_access, value_expr in op.items:
# Evaluate the target (should be a PropertyAccess node)
if hasattr(property_access, "variable") and hasattr(property_access, "property"):
var_name = (
property_access.variable.name
if hasattr(property_access.variable, "name")
else property_access.variable
)
prop_name = property_access.property

# Get the node or edge from context
if var_name in ctx.bindings:
element = ctx.bindings[var_name]

# Evaluate the new value
new_value = evaluate_expression(value_expr, ctx)

# Update the property on the element
# Note: This modifies the element in place in the graph
element.properties[prop_name] = new_value

self._execute_set_items(op.items, ctx)
result.append(ctx)

return result
Expand Down Expand Up @@ -1187,12 +1166,13 @@ def _execute_delete(
def _execute_merge(
self, op: Merge, input_rows: list[ExecutionContext]
) -> list[ExecutionContext]:
"""Execute MERGE operator.
"""Execute MERGE operator with ON CREATE SET support.

Creates patterns if they don't exist, or matches them if they do.
Conditionally executes SET operations based on whether elements were created.

Args:
op: Merge operator with patterns
op: Merge operator with patterns and optional on_create clause
input_rows: Input execution contexts

Returns:
Expand All @@ -1210,6 +1190,9 @@ def _execute_merge(
new_ctx = ExecutionContext()
new_ctx.bindings = ctx.bindings.copy()

# Track whether we created anything (for ON CREATE SET)
was_created = False

# Process each pattern
for pattern in op.patterns:
if not pattern:
Expand Down Expand Up @@ -1261,17 +1244,50 @@ def _execute_merge(

# Bind found node or create new one
if found_node:
was_created = False
if node_pattern.variable:
new_ctx.bindings[node_pattern.variable] = found_node
else:
was_created = True
node = self._create_node_from_pattern(node_pattern, new_ctx)
if node_pattern.variable:
new_ctx.bindings[node_pattern.variable] = node

# Execute conditional SET if we created something
if was_created and op.on_create:
self._execute_set_items(op.on_create.items, new_ctx)

result.append(new_ctx)

return result

def _execute_set_items(self, items: list, ctx: ExecutionContext) -> None:
"""Execute SET items on a context (helper for conditional SET).

Args:
items: List of (property_access, expression) tuples
ctx: Execution context to update
"""
for property_access, value_expr in items:
# Evaluate the target (should be a PropertyAccess node)
if hasattr(property_access, "variable") and hasattr(property_access, "property"):
var_name = (
property_access.variable.name
if hasattr(property_access.variable, "name")
else property_access.variable
)
prop_name = property_access.property

# Get the node or edge from context
if var_name in ctx.bindings:
element = ctx.bindings[var_name]

# Evaluate the new value
new_value = evaluate_expression(value_expr, ctx)

# Update the property on the element
element.properties[prop_name] = new_value

def _execute_unwind(
self, op: Unwind, input_rows: list[ExecutionContext]
) -> list[ExecutionContext]:
Expand Down
6 changes: 5 additions & 1 deletion src/graphforge/parser/cypher.lark
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ delete_clause: "DETACH"i "DELETE"i variable ("," variable)* -> detach_delete
| "DELETE"i variable ("," variable)* -> regular_delete

// MERGE clause
merge_clause: "MERGE"i pattern ("," pattern)*
merge_clause: "MERGE"i pattern ("," pattern)* merge_action*

merge_action: on_create_clause

on_create_clause: "ON"i "CREATE"i set_clause

pattern: node_pattern (relationship_pattern node_pattern)*

Expand Down
24 changes: 21 additions & 3 deletions src/graphforge/parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,27 @@ def regular_delete(self, items):
return DeleteClause(variables=variables, detach=False)

def merge_clause(self, items):
"""Transform MERGE clause."""
patterns = [item for item in items if not isinstance(item, str)]
return MergeClause(patterns=patterns)
"""Transform MERGE clause with optional ON CREATE action."""
patterns = []
on_create = None

for item in items:
if isinstance(item, str):
continue
if isinstance(item, tuple) and item[0] == "on_create":
on_create = item[1]
else:
patterns.append(item)

return MergeClause(patterns=patterns, on_create=on_create)

def merge_action(self, items):
"""Transform merge action."""
return items[0]

def on_create_clause(self, items):
"""Transform ON CREATE SET clause."""
return ("on_create", items[0])

def unwind_clause(self, items):
"""Transform UNWIND clause."""
Expand Down
5 changes: 4 additions & 1 deletion src/graphforge/planner/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,18 @@ class Delete:

@dataclass
class Merge:
"""Operator for merging patterns.
"""Operator for merging patterns with conditional SET support.

Creates patterns if they don't exist, or matches them if they do.
Optionally executes SET operations only when creating (ON CREATE SET).

Attributes:
patterns: List of patterns to merge
on_create: Optional SetClause to execute when creating new elements
"""

patterns: list[Any] # List of node and relationship patterns to merge
on_create: Any = None # Optional SetClause for ON CREATE SET


@dataclass
Expand Down
2 changes: 1 addition & 1 deletion src/graphforge/planner/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def _plan_simple_query(self, clauses: list) -> list:

# 4. MERGE
for merge in merge_clauses:
operators.append(Merge(patterns=merge.patterns))
operators.append(Merge(patterns=merge.patterns, on_create=merge.on_create))

# 5. WHERE
if where_clause:
Expand Down
Loading
Loading