Copilot commented on code in PR #2347:
URL: https://github.com/apache/age/pull/2347#discussion_r2873984555


##########
regress/sql/cypher_merge.sql:
##########
@@ -868,9 +868,87 @@ SELECT * FROM cypher('issue_1630', $$ MATCH (n) DETACH 
DELETE n $$) AS (a agtype
 SELECT * FROM cypher('issue_1709', $$ MATCH (n) DETACH DELETE n $$) AS (a 
agtype);
 SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a 
agtype);
 
+--
+-- ON CREATE SET / ON MATCH SET tests (issue #1619)
+--
+SELECT create_graph('merge_actions');
+
+-- Basic ON CREATE SET: first run creates the node
+SELECT * FROM cypher('merge_actions', $$
+  MERGE (n:Person {name: 'Alice'})
+    ON CREATE SET n.created = true
+  RETURN n.name, n.created
+$$) AS (name agtype, created agtype);
+
+-- ON MATCH SET: second run matches the existing node
+SELECT * FROM cypher('merge_actions', $$
+  MERGE (n:Person {name: 'Alice'})
+    ON MATCH SET n.found = true
+  RETURN n.name, n.created, n.found
+$$) AS (name agtype, created agtype, found agtype);
+
+-- Both ON CREATE SET and ON MATCH SET (first run = create)
+SELECT * FROM cypher('merge_actions', $$
+  MERGE (n:Person {name: 'Bob'})
+    ON CREATE SET n.created = true
+    ON MATCH SET n.matched = true
+  RETURN n.name, n.created, n.matched
+$$) AS (name agtype, created agtype, matched agtype);
+
+-- Both ON CREATE SET and ON MATCH SET (second run = match)
+SELECT * FROM cypher('merge_actions', $$
+  MERGE (n:Person {name: 'Bob'})
+    ON CREATE SET n.created = true
+    ON MATCH SET n.matched = true
+  RETURN n.name, n.created, n.matched
+$$) AS (name agtype, created agtype, matched agtype);
+
+-- ON MATCH SET with MERGE after MATCH (Case 1: has predecessor)
+SELECT * FROM cypher('merge_actions', $$
+  MATCH (a:Person {name: 'Alice'})
+  MERGE (a)-[:KNOWS]->(b:Person {name: 'Charlie'})
+    ON CREATE SET b.source = 'merge_create'
+  RETURN a.name, b.name, b.source
+$$) AS (a agtype, b agtype, source agtype);
+

Review Comment:
   The test labeled "ON MATCH SET with MERGE after MATCH (Case 1: has 
predecessor)" only tests `ON CREATE SET` in Case 1 (MERGE with a predecessor 
clause). There is no test that verifies `ON MATCH SET` behavior in Case 1 — 
specifically, running a `MATCH ... MERGE ... ON MATCH SET` query where the path 
already exists and the ON MATCH SET clause fires. This execution path (lines 
682-685 and 704-708 in `cypher_merge.c`) is untested.
   ```suggestion
   -- ON MATCH SET with MERGE after MATCH (Case 1: has predecessor, first run = 
create)
   SELECT * FROM cypher('merge_actions', $$
     MATCH (a:Person {name: 'Alice'})
     MERGE (a)-[:KNOWS]->(b:Person {name: 'Charlie'})
       ON CREATE SET b.source = 'merge_create'
       ON MATCH SET b.source = 'merge_match'
     RETURN a.name, b.name, b.source
   $$) AS (a agtype, b agtype, source agtype);
   
   -- ON MATCH SET with MERGE after MATCH (Case 1: has predecessor, second run 
= match)
   SELECT * FROM cypher('merge_actions', $$
     MATCH (a:Person {name: 'Alice'})
     MERGE (a)-[:KNOWS]->(b:Person {name: 'Charlie'})
       ON CREATE SET b.source = 'merge_create'
       ON MATCH SET b.source = 'merge_match'
     RETURN a.name, b.name, b.source
   $$) AS (a agtype, b agtype, source agtype);
   ```



##########
src/backend/executor/cypher_set.c:
##########
@@ -485,11 +489,31 @@ static void process_update_list(CustomScanState *node)
          * this is a REMOVE clause or the variable references a variable that 
is
          * NULL. It will be possible for a variable to be NULL when OPTIONAL
          * MATCH is implemented.
+         *
+         * If prop_expr is set (used by MERGE ON CREATE/MATCH SET), evaluate
+         * the expression directly rather than reading from the scan tuple.
+         * The planner may have stripped the target entry at prop_position.
          */
         if (update_item->remove_item)
         {
             remove_property = true;
         }
+        else if (update_item->prop_expr != NULL)
+        {
+            ExprState *expr_state;
+            Datum val;
+            bool isnull;
+
+            expr_state = ExecInitExpr((Expr *)update_item->prop_expr,
+                                      (PlanState *)node);
+            val = ExecEvalExpr(expr_state, econtext, &isnull);

Review Comment:
   `ExecInitExpr` is called on every invocation of `apply_update_list` (once 
per row in the non-terminal MERGE path, lines 640-720 in `cypher_merge.c`). 
Expression state initialization should happen once at plan initialization time 
(in `create_cypher_merge_plan_state`), not per row. 
   
   The established pattern in this codebase is to call `ExecInitExpr` once 
during the plan state setup (see `cypher_merge.c:175-185` and 
`cypher_create.c:115-125`). Pre-initialized `ExprState` objects should be 
stored alongside `cypher_update_item` (e.g., a parallel `prop_expr_state` 
field) or in the `cypher_merge_custom_scan_state`. Calling `ExecInitExpr` on 
each row incurs unnecessary memory allocation and state-building overhead that 
grows linearly with the number of matched or created entities.



##########
regress/sql/cypher_merge.sql:
##########
@@ -868,9 +868,87 @@ SELECT * FROM cypher('issue_1630', $$ MATCH (n) DETACH 
DELETE n $$) AS (a agtype
 SELECT * FROM cypher('issue_1709', $$ MATCH (n) DETACH DELETE n $$) AS (a 
agtype);
 SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a 
agtype);
 
+--
+-- ON CREATE SET / ON MATCH SET tests (issue #1619)
+--
+SELECT create_graph('merge_actions');
+
+-- Basic ON CREATE SET: first run creates the node
+SELECT * FROM cypher('merge_actions', $$
+  MERGE (n:Person {name: 'Alice'})
+    ON CREATE SET n.created = true
+  RETURN n.name, n.created
+$$) AS (name agtype, created agtype);
+
+-- ON MATCH SET: second run matches the existing node
+SELECT * FROM cypher('merge_actions', $$
+  MERGE (n:Person {name: 'Alice'})
+    ON MATCH SET n.found = true
+  RETURN n.name, n.created, n.found
+$$) AS (name agtype, created agtype, found agtype);
+
+-- Both ON CREATE SET and ON MATCH SET (first run = create)
+SELECT * FROM cypher('merge_actions', $$
+  MERGE (n:Person {name: 'Bob'})
+    ON CREATE SET n.created = true
+    ON MATCH SET n.matched = true
+  RETURN n.name, n.created, n.matched
+$$) AS (name agtype, created agtype, matched agtype);
+
+-- Both ON CREATE SET and ON MATCH SET (second run = match)
+SELECT * FROM cypher('merge_actions', $$
+  MERGE (n:Person {name: 'Bob'})
+    ON CREATE SET n.created = true
+    ON MATCH SET n.matched = true
+  RETURN n.name, n.created, n.matched
+$$) AS (name agtype, created agtype, matched agtype);
+
+-- ON MATCH SET with MERGE after MATCH (Case 1: has predecessor)

Review Comment:
   The comment says "ON MATCH SET with MERGE after MATCH (Case 1: has 
predecessor)" but the test only uses `ON CREATE SET`. The comment is misleading 
— it should say "ON CREATE SET with MERGE after MATCH" or the test should also 
cover the ON MATCH SET path by re-running the same MERGE query so that the path 
is found on the second run, triggering ON MATCH SET.
   ```suggestion
   -- ON CREATE SET with MERGE after MATCH (Case 1: has predecessor)
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to