Skip to content
Open
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
131 changes: 131 additions & 0 deletions regress/expected/cypher_merge.out
Original file line number Diff line number Diff line change
Expand Up @@ -1717,6 +1717,115 @@ SELECT * FROM cypher('issue_1907', $$ MATCH ()-[r]->() RETURN r $$) AS (r agtype
{"id": 1125899906842626, "label": "RELATED_TO", "end_id": 281474976710660, "start_id": 281474976710659, "properties": {"property1": "something", "property2": "else"}}::edge
(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
--
Expand All @@ -1735,6 +1844,11 @@ SELECT * FROM cypher('issue_1709', $$ 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
--
Expand Down Expand Up @@ -1812,6 +1926,23 @@ NOTICE: graph "issue_1709" 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
--
66 changes: 66 additions & 0 deletions regress/sql/cypher_merge.sql
Original file line number Diff line number Diff line change
Expand Up @@ -785,12 +785,77 @@ SELECT * FROM cypher('issue_1907', $$ MERGE (a {name: 'Test Node A'})-[r:RELATED
-- should return properties added
SELECT * FROM cypher('issue_1907', $$ MATCH ()-[r]->() RETURN r $$) AS (r 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
--
SELECT * FROM cypher('cypher_merge', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
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_1954', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);

--
-- delete graphs
Expand All @@ -800,6 +865,7 @@ SELECT drop_graph('cypher_merge', true);
SELECT drop_graph('issue_1630', true);
SELECT drop_graph('issue_1691', true);
SELECT drop_graph('issue_1709', true);
SELECT drop_graph('issue_1954', true);

--
-- End
Expand Down
21 changes: 21 additions & 0 deletions src/backend/executor/cypher_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));

Comment on lines 236 to 239
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

estate->es_snapshot->curcid can be ahead of the global command ID (e.g., Increment_Estate_CommandId() increments es_snapshot->curcid without changing GetCurrentCommandId()). Unconditionally assigning GetCurrentCommandId(false) here can decrease curcid and make tuples with Cmin == saved_curcid - 1 invisible during the scan, potentially causing false "was deleted" errors. Consider only advancing curcid when the global CID is greater than the current snapshot CID (e.g., use max(saved_curcid, GetCurrentCommandId(false)) or a conditional update), or use a local snapshot copy with an adjusted curcid instead of mutating the shared snapshot.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a1fee6d — changed to Max(saved_curcid, GetCurrentCommandId(false)) so we only ever increase curcid, never decrease it.

rel = table_open(label->relation, RowExclusiveLock);
scan_desc = table_beginscan(rel, estate->es_snapshot, 1, scan_keys);

Expand All @@ -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;
}

Expand Down