diff --git a/regress/expected/cypher_merge.out b/regress/expected/cypher_merge.out index 8c37dc2de..4242f2f59 100644 --- a/regress/expected/cypher_merge.out +++ b/regress/expected/cypher_merge.out @@ -1865,6 +1865,114 @@ $$) AS (edge_count agtype); 0 (1 row) +-- Issue 1954: CREATE + WITH + MERGE causes "vertex was deleted" error +-- when the number of input rows exceeds the snapshot's command ID window. +-- entity_exists() used a stale curcid, making recently-created vertices +-- invisible on later iterations. +-- +SELECT * FROM create_graph('issue_1954'); +NOTICE: graph "issue_1954" has been created + create_graph +-------------- + +(1 row) + +-- Setup: create source nodes and relationships (3 rows to trigger the bug) +SELECT * FROM cypher('issue_1954', $$ + CREATE (:A {name: 'a1'})-[:R]->(:B {name: 'b1'}), + (:A {name: 'a2'})-[:R]->(:B {name: 'b2'}), + (:A {name: 'a3'})-[:R]->(:B {name: 'b3'}) +$$) AS (result agtype); + result +-------- +(0 rows) + +-- This query would fail with "vertex assigned to variable c was deleted" +-- on the 3rd row before the fix. +SELECT * FROM cypher('issue_1954', $$ + MATCH (a:A)-[:R]->(b:B) + CREATE (c:C {name: a.name + '|' + b.name}) + WITH a, b, c + MERGE (a)-[:LINK]->(c) + RETURN a.name, b.name, c.name + ORDER BY a.name +$$) AS (a agtype, b agtype, c agtype); + a | b | c +------+------+--------- + "a1" | "b1" | "a1|b1" + "a2" | "b2" | "a2|b2" + "a3" | "b3" | "a3|b3" +(3 rows) + +-- Verify edges were created +SELECT * FROM cypher('issue_1954', $$ + MATCH (a:A)-[:LINK]->(c:C) + RETURN a.name, c.name + ORDER BY a.name +$$) AS (a agtype, c agtype); + a | c +------+--------- + "a1" | "a1|b1" + "a2" | "a2|b2" + "a3" | "a3|b3" +(3 rows) + +-- Test with two MERGEs (more complex case from the original report) +SELECT * FROM cypher('issue_1954', $$ + MATCH ()-[e:LINK]->() DELETE e +$$) AS (result agtype); + result +-------- +(0 rows) + +SELECT * FROM cypher('issue_1954', $$ + MATCH (c:C) DELETE c +$$) AS (result agtype); + result +-------- +(0 rows) + +SELECT * FROM cypher('issue_1954', $$ + MATCH (a:A)-[:R]->(b:B) + CREATE (c:C {name: a.name + '|' + b.name}) + WITH a, b, c + MERGE (a)-[:LINK1]->(c) + MERGE (b)-[:LINK2]->(c) + RETURN a.name, b.name, c.name + ORDER BY a.name +$$) AS (a agtype, b agtype, c agtype); + a | b | c +------+------+--------- + "a1" | "b1" | "a1|b1" + "a2" | "b2" | "a2|b2" + "a3" | "b3" | "a3|b3" +(3 rows) + +-- Verify both sets of edges +SELECT * FROM cypher('issue_1954', $$ + MATCH (a:A)-[:LINK1]->(c:C) + RETURN a.name, c.name + ORDER BY a.name +$$) AS (a agtype, c agtype); + a | c +------+--------- + "a1" | "a1|b1" + "a2" | "a2|b2" + "a3" | "a3|b3" +(3 rows) + +SELECT * FROM cypher('issue_1954', $$ + MATCH (b:B)-[:LINK2]->(c:C) + RETURN b.name, c.name + ORDER BY b.name +$$) AS (b agtype, c agtype); + b | c +------+--------- + "b1" | "a1|b1" + "b2" | "a2|b2" + "b3" | "a3|b3" +(3 rows) + -- -- clean up graphs -- @@ -1888,6 +1996,11 @@ SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype --- (0 rows) +SELECT * FROM cypher('issue_1954', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype); + a +--- +(0 rows) + -- -- delete graphs -- @@ -1985,6 +2098,23 @@ NOTICE: graph "issue_1446" has been dropped (1 row) +SELECT drop_graph('issue_1954', true); +NOTICE: drop cascades to 9 other objects +DETAIL: drop cascades to table issue_1954._ag_label_vertex +drop cascades to table issue_1954._ag_label_edge +drop cascades to table issue_1954."A" +drop cascades to table issue_1954."R" +drop cascades to table issue_1954."B" +drop cascades to table issue_1954."C" +drop cascades to table issue_1954."LINK" +drop cascades to table issue_1954."LINK1" +drop cascades to table issue_1954."LINK2" +NOTICE: graph "issue_1954" has been dropped + drop_graph +------------ + +(1 row) + -- -- End -- diff --git a/regress/sql/cypher_merge.sql b/regress/sql/cypher_merge.sql index cc900e73d..5939c42a8 100644 --- a/regress/sql/cypher_merge.sql +++ b/regress/sql/cypher_merge.sql @@ -860,6 +860,69 @@ SELECT * FROM cypher('issue_1446', $$ RETURN count(*) AS edge_count $$) AS (edge_count agtype); +-- Issue 1954: CREATE + WITH + MERGE causes "vertex was deleted" error +-- when the number of input rows exceeds the snapshot's command ID window. +-- entity_exists() used a stale curcid, making recently-created vertices +-- invisible on later iterations. +-- +SELECT * FROM create_graph('issue_1954'); + +-- Setup: create source nodes and relationships (3 rows to trigger the bug) +SELECT * FROM cypher('issue_1954', $$ + CREATE (:A {name: 'a1'})-[:R]->(:B {name: 'b1'}), + (:A {name: 'a2'})-[:R]->(:B {name: 'b2'}), + (:A {name: 'a3'})-[:R]->(:B {name: 'b3'}) +$$) AS (result agtype); + +-- This query would fail with "vertex assigned to variable c was deleted" +-- on the 3rd row before the fix. +SELECT * FROM cypher('issue_1954', $$ + MATCH (a:A)-[:R]->(b:B) + CREATE (c:C {name: a.name + '|' + b.name}) + WITH a, b, c + MERGE (a)-[:LINK]->(c) + RETURN a.name, b.name, c.name + ORDER BY a.name +$$) AS (a agtype, b agtype, c agtype); + +-- Verify edges were created +SELECT * FROM cypher('issue_1954', $$ + MATCH (a:A)-[:LINK]->(c:C) + RETURN a.name, c.name + ORDER BY a.name +$$) AS (a agtype, c agtype); + +-- Test with two MERGEs (more complex case from the original report) +SELECT * FROM cypher('issue_1954', $$ + MATCH ()-[e:LINK]->() DELETE e +$$) AS (result agtype); +SELECT * FROM cypher('issue_1954', $$ + MATCH (c:C) DELETE c +$$) AS (result agtype); + +SELECT * FROM cypher('issue_1954', $$ + MATCH (a:A)-[:R]->(b:B) + CREATE (c:C {name: a.name + '|' + b.name}) + WITH a, b, c + MERGE (a)-[:LINK1]->(c) + MERGE (b)-[:LINK2]->(c) + RETURN a.name, b.name, c.name + ORDER BY a.name +$$) AS (a agtype, b agtype, c agtype); + +-- Verify both sets of edges +SELECT * FROM cypher('issue_1954', $$ + MATCH (a:A)-[:LINK1]->(c:C) + RETURN a.name, c.name + ORDER BY a.name +$$) AS (a agtype, c agtype); + +SELECT * FROM cypher('issue_1954', $$ + MATCH (b:B)-[:LINK2]->(c:C) + RETURN b.name, c.name + ORDER BY b.name +$$) AS (b agtype, c agtype); + -- -- clean up graphs -- @@ -867,6 +930,7 @@ SELECT * FROM cypher('cypher_merge', $$ MATCH (n) DETACH DELETE n $$) AS (a agty 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); +SELECT * FROM cypher('issue_1954', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype); -- -- delete graphs @@ -877,6 +941,7 @@ SELECT drop_graph('issue_1630', true); SELECT drop_graph('issue_1691', true); SELECT drop_graph('issue_1709', true); SELECT drop_graph('issue_1446', true); +SELECT drop_graph('issue_1954', true); -- -- End diff --git a/src/backend/executor/cypher_utils.c b/src/backend/executor/cypher_utils.c index eff829925..940284234 100644 --- a/src/backend/executor/cypher_utils.c +++ b/src/backend/executor/cypher_utils.c @@ -208,6 +208,7 @@ bool entity_exists(EState *estate, Oid graph_oid, graphid id) HeapTuple tuple; Relation rel; bool result = true; + CommandId saved_curcid; /* * Extract the label id from the graph id and get the table name @@ -219,6 +220,23 @@ bool entity_exists(EState *estate, Oid graph_oid, graphid id) ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber, F_GRAPHIDEQ, GRAPHID_GET_DATUM(id)); + /* + * Temporarily advance the snapshot's curcid so that entities inserted + * by preceding clauses (e.g., CREATE) in the same query are visible. + * CREATE calls CommandCounterIncrement() which advances the global + * CID, but does not update es_snapshot->curcid. The Decrement/Increment + * CID macros used by the executors can leave curcid behind the global + * CID, making recently created entities invisible to this scan. + * + * Use Max to ensure we never decrease curcid. The executor macros + * (Increment_Estate_CommandId) can push curcid above the global CID, + * and blindly assigning GetCurrentCommandId could make tuples that + * are visible at the current curcid become invisible. + */ + saved_curcid = estate->es_snapshot->curcid; + estate->es_snapshot->curcid = Max(saved_curcid, + GetCurrentCommandId(false)); + rel = table_open(label->relation, RowExclusiveLock); scan_desc = table_beginscan(rel, estate->es_snapshot, 1, scan_keys); @@ -236,6 +254,9 @@ bool entity_exists(EState *estate, Oid graph_oid, graphid id) table_endscan(scan_desc); table_close(rel, RowExclusiveLock); + /* Restore the original curcid */ + estate->es_snapshot->curcid = saved_curcid; + return result; }