Skip to content

Commit d0741d8

Browse files
gregfeliceclaude
andauthored
Fix entity_exists() CID visibility for CREATE + WITH + MERGE (#2343)
* Fix entity_exists() CID visibility for CREATE + WITH + MERGE (#1954) When a Cypher query chains CREATE ... WITH ... MERGE, vertices created by CREATE become invisible to entity_exists() after a threshold number of input rows. This causes MERGE to throw "vertex assigned to variable was deleted". Root cause: CREATE calls CommandCounterIncrement() which advances the global command ID, but does not update es_snapshot->curcid. The Decrement/Increment CID macros used by the executors bring curcid back to the same value on each iteration. After enough rows, newly inserted vertices have a Cmin >= curcid and HeapTupleSatisfiesMVCC rejects them (requires Cmin < curcid). Fix: In entity_exists(), temporarily set es_snapshot->curcid to the current global command ID (via GetCurrentCommandId) for the duration of the scan, then restore it. This makes all entities inserted by preceding clauses in the same query visible to the existence check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use Max() to prevent curcid regression in entity_exists() Address review feedback: es_snapshot->curcid can be ahead of the global CID due to Increment_Estate_CommandId macros. Unconditionally assigning GetCurrentCommandId(false) could decrease curcid, making previously visible tuples invisible. Use Max(saved_curcid, GetCurrentCommandId(false)) to ensure we only ever increase visibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a21120b commit d0741d8

File tree

3 files changed

+216
-0
lines changed

3 files changed

+216
-0
lines changed

regress/expected/cypher_merge.out

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,6 +1865,114 @@ $$) AS (edge_count agtype);
18651865
0
18661866
(1 row)
18671867

1868+
-- Issue 1954: CREATE + WITH + MERGE causes "vertex was deleted" error
1869+
-- when the number of input rows exceeds the snapshot's command ID window.
1870+
-- entity_exists() used a stale curcid, making recently-created vertices
1871+
-- invisible on later iterations.
1872+
--
1873+
SELECT * FROM create_graph('issue_1954');
1874+
NOTICE: graph "issue_1954" has been created
1875+
create_graph
1876+
--------------
1877+
1878+
(1 row)
1879+
1880+
-- Setup: create source nodes and relationships (3 rows to trigger the bug)
1881+
SELECT * FROM cypher('issue_1954', $$
1882+
CREATE (:A {name: 'a1'})-[:R]->(:B {name: 'b1'}),
1883+
(:A {name: 'a2'})-[:R]->(:B {name: 'b2'}),
1884+
(:A {name: 'a3'})-[:R]->(:B {name: 'b3'})
1885+
$$) AS (result agtype);
1886+
result
1887+
--------
1888+
(0 rows)
1889+
1890+
-- This query would fail with "vertex assigned to variable c was deleted"
1891+
-- on the 3rd row before the fix.
1892+
SELECT * FROM cypher('issue_1954', $$
1893+
MATCH (a:A)-[:R]->(b:B)
1894+
CREATE (c:C {name: a.name + '|' + b.name})
1895+
WITH a, b, c
1896+
MERGE (a)-[:LINK]->(c)
1897+
RETURN a.name, b.name, c.name
1898+
ORDER BY a.name
1899+
$$) AS (a agtype, b agtype, c agtype);
1900+
a | b | c
1901+
------+------+---------
1902+
"a1" | "b1" | "a1|b1"
1903+
"a2" | "b2" | "a2|b2"
1904+
"a3" | "b3" | "a3|b3"
1905+
(3 rows)
1906+
1907+
-- Verify edges were created
1908+
SELECT * FROM cypher('issue_1954', $$
1909+
MATCH (a:A)-[:LINK]->(c:C)
1910+
RETURN a.name, c.name
1911+
ORDER BY a.name
1912+
$$) AS (a agtype, c agtype);
1913+
a | c
1914+
------+---------
1915+
"a1" | "a1|b1"
1916+
"a2" | "a2|b2"
1917+
"a3" | "a3|b3"
1918+
(3 rows)
1919+
1920+
-- Test with two MERGEs (more complex case from the original report)
1921+
SELECT * FROM cypher('issue_1954', $$
1922+
MATCH ()-[e:LINK]->() DELETE e
1923+
$$) AS (result agtype);
1924+
result
1925+
--------
1926+
(0 rows)
1927+
1928+
SELECT * FROM cypher('issue_1954', $$
1929+
MATCH (c:C) DELETE c
1930+
$$) AS (result agtype);
1931+
result
1932+
--------
1933+
(0 rows)
1934+
1935+
SELECT * FROM cypher('issue_1954', $$
1936+
MATCH (a:A)-[:R]->(b:B)
1937+
CREATE (c:C {name: a.name + '|' + b.name})
1938+
WITH a, b, c
1939+
MERGE (a)-[:LINK1]->(c)
1940+
MERGE (b)-[:LINK2]->(c)
1941+
RETURN a.name, b.name, c.name
1942+
ORDER BY a.name
1943+
$$) AS (a agtype, b agtype, c agtype);
1944+
a | b | c
1945+
------+------+---------
1946+
"a1" | "b1" | "a1|b1"
1947+
"a2" | "b2" | "a2|b2"
1948+
"a3" | "b3" | "a3|b3"
1949+
(3 rows)
1950+
1951+
-- Verify both sets of edges
1952+
SELECT * FROM cypher('issue_1954', $$
1953+
MATCH (a:A)-[:LINK1]->(c:C)
1954+
RETURN a.name, c.name
1955+
ORDER BY a.name
1956+
$$) AS (a agtype, c agtype);
1957+
a | c
1958+
------+---------
1959+
"a1" | "a1|b1"
1960+
"a2" | "a2|b2"
1961+
"a3" | "a3|b3"
1962+
(3 rows)
1963+
1964+
SELECT * FROM cypher('issue_1954', $$
1965+
MATCH (b:B)-[:LINK2]->(c:C)
1966+
RETURN b.name, c.name
1967+
ORDER BY b.name
1968+
$$) AS (b agtype, c agtype);
1969+
b | c
1970+
------+---------
1971+
"b1" | "a1|b1"
1972+
"b2" | "a2|b2"
1973+
"b3" | "a3|b3"
1974+
(3 rows)
1975+
18681976
--
18691977
-- clean up graphs
18701978
--
@@ -1888,6 +1996,11 @@ SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype
18881996
---
18891997
(0 rows)
18901998

1999+
SELECT * FROM cypher('issue_1954', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
2000+
a
2001+
---
2002+
(0 rows)
2003+
18912004
--
18922005
-- delete graphs
18932006
--
@@ -1985,6 +2098,23 @@ NOTICE: graph "issue_1446" has been dropped
19852098

19862099
(1 row)
19872100

2101+
SELECT drop_graph('issue_1954', true);
2102+
NOTICE: drop cascades to 9 other objects
2103+
DETAIL: drop cascades to table issue_1954._ag_label_vertex
2104+
drop cascades to table issue_1954._ag_label_edge
2105+
drop cascades to table issue_1954."A"
2106+
drop cascades to table issue_1954."R"
2107+
drop cascades to table issue_1954."B"
2108+
drop cascades to table issue_1954."C"
2109+
drop cascades to table issue_1954."LINK"
2110+
drop cascades to table issue_1954."LINK1"
2111+
drop cascades to table issue_1954."LINK2"
2112+
NOTICE: graph "issue_1954" has been dropped
2113+
drop_graph
2114+
------------
2115+
2116+
(1 row)
2117+
19882118
--
19892119
-- End
19902120
--

regress/sql/cypher_merge.sql

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,13 +860,77 @@ SELECT * FROM cypher('issue_1446', $$
860860
RETURN count(*) AS edge_count
861861
$$) AS (edge_count agtype);
862862

863+
-- Issue 1954: CREATE + WITH + MERGE causes "vertex was deleted" error
864+
-- when the number of input rows exceeds the snapshot's command ID window.
865+
-- entity_exists() used a stale curcid, making recently-created vertices
866+
-- invisible on later iterations.
867+
--
868+
SELECT * FROM create_graph('issue_1954');
869+
870+
-- Setup: create source nodes and relationships (3 rows to trigger the bug)
871+
SELECT * FROM cypher('issue_1954', $$
872+
CREATE (:A {name: 'a1'})-[:R]->(:B {name: 'b1'}),
873+
(:A {name: 'a2'})-[:R]->(:B {name: 'b2'}),
874+
(:A {name: 'a3'})-[:R]->(:B {name: 'b3'})
875+
$$) AS (result agtype);
876+
877+
-- This query would fail with "vertex assigned to variable c was deleted"
878+
-- on the 3rd row before the fix.
879+
SELECT * FROM cypher('issue_1954', $$
880+
MATCH (a:A)-[:R]->(b:B)
881+
CREATE (c:C {name: a.name + '|' + b.name})
882+
WITH a, b, c
883+
MERGE (a)-[:LINK]->(c)
884+
RETURN a.name, b.name, c.name
885+
ORDER BY a.name
886+
$$) AS (a agtype, b agtype, c agtype);
887+
888+
-- Verify edges were created
889+
SELECT * FROM cypher('issue_1954', $$
890+
MATCH (a:A)-[:LINK]->(c:C)
891+
RETURN a.name, c.name
892+
ORDER BY a.name
893+
$$) AS (a agtype, c agtype);
894+
895+
-- Test with two MERGEs (more complex case from the original report)
896+
SELECT * FROM cypher('issue_1954', $$
897+
MATCH ()-[e:LINK]->() DELETE e
898+
$$) AS (result agtype);
899+
SELECT * FROM cypher('issue_1954', $$
900+
MATCH (c:C) DELETE c
901+
$$) AS (result agtype);
902+
903+
SELECT * FROM cypher('issue_1954', $$
904+
MATCH (a:A)-[:R]->(b:B)
905+
CREATE (c:C {name: a.name + '|' + b.name})
906+
WITH a, b, c
907+
MERGE (a)-[:LINK1]->(c)
908+
MERGE (b)-[:LINK2]->(c)
909+
RETURN a.name, b.name, c.name
910+
ORDER BY a.name
911+
$$) AS (a agtype, b agtype, c agtype);
912+
913+
-- Verify both sets of edges
914+
SELECT * FROM cypher('issue_1954', $$
915+
MATCH (a:A)-[:LINK1]->(c:C)
916+
RETURN a.name, c.name
917+
ORDER BY a.name
918+
$$) AS (a agtype, c agtype);
919+
920+
SELECT * FROM cypher('issue_1954', $$
921+
MATCH (b:B)-[:LINK2]->(c:C)
922+
RETURN b.name, c.name
923+
ORDER BY b.name
924+
$$) AS (b agtype, c agtype);
925+
863926
--
864927
-- clean up graphs
865928
--
866929
SELECT * FROM cypher('cypher_merge', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
867930
SELECT * FROM cypher('issue_1630', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
868931
SELECT * FROM cypher('issue_1709', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
869932
SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
933+
SELECT * FROM cypher('issue_1954', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
870934

871935
--
872936
-- delete graphs
@@ -877,6 +941,7 @@ SELECT drop_graph('issue_1630', true);
877941
SELECT drop_graph('issue_1691', true);
878942
SELECT drop_graph('issue_1709', true);
879943
SELECT drop_graph('issue_1446', true);
944+
SELECT drop_graph('issue_1954', true);
880945

881946
--
882947
-- End

src/backend/executor/cypher_utils.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ bool entity_exists(EState *estate, Oid graph_oid, graphid id)
208208
HeapTuple tuple;
209209
Relation rel;
210210
bool result = true;
211+
CommandId saved_curcid;
211212

212213
/*
213214
* 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)
219220
ScanKeyInit(&scan_keys[0], 1, BTEqualStrategyNumber,
220221
F_GRAPHIDEQ, GRAPHID_GET_DATUM(id));
221222

223+
/*
224+
* Temporarily advance the snapshot's curcid so that entities inserted
225+
* by preceding clauses (e.g., CREATE) in the same query are visible.
226+
* CREATE calls CommandCounterIncrement() which advances the global
227+
* CID, but does not update es_snapshot->curcid. The Decrement/Increment
228+
* CID macros used by the executors can leave curcid behind the global
229+
* CID, making recently created entities invisible to this scan.
230+
*
231+
* Use Max to ensure we never decrease curcid. The executor macros
232+
* (Increment_Estate_CommandId) can push curcid above the global CID,
233+
* and blindly assigning GetCurrentCommandId could make tuples that
234+
* are visible at the current curcid become invisible.
235+
*/
236+
saved_curcid = estate->es_snapshot->curcid;
237+
estate->es_snapshot->curcid = Max(saved_curcid,
238+
GetCurrentCommandId(false));
239+
222240
rel = table_open(label->relation, RowExclusiveLock);
223241
scan_desc = table_beginscan(rel, estate->es_snapshot, 1, scan_keys);
224242

@@ -236,6 +254,9 @@ bool entity_exists(EState *estate, Oid graph_oid, graphid id)
236254
table_endscan(scan_desc);
237255
table_close(rel, RowExclusiveLock);
238256

257+
/* Restore the original curcid */
258+
estate->es_snapshot->curcid = saved_curcid;
259+
239260
return result;
240261
}
241262

0 commit comments

Comments
 (0)