Skip to content

Commit 52c21d4

Browse files
gregfeliceclaude
andcommitted
Fix chained MERGE not seeing sibling MERGE's changes (#1446)
When multiple MERGEs are chained (e.g. MATCH ... MERGE ... MERGE ...), the non-terminal (first) MERGE returned rows one at a time to the parent plan node. The parent MERGE's lateral join would materialize its hash table on the first row, before the child MERGE had finished all its iterations. This caused the second MERGE to not see entities created by the first MERGE, leading to duplicate nodes. Fix by making non-terminal MERGE eager: it processes ALL input rows and buffers the projected results before returning any to the parent. This ensures all entity creations are committed before any parent plan node scans the tables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 806fa2e commit 52c21d4

File tree

4 files changed

+321
-51
lines changed

4 files changed

+321
-51
lines changed

regress/expected/cypher_merge.out

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,6 +1717,116 @@ SELECT * FROM cypher('issue_1907', $$ MATCH ()-[r]->() RETURN r $$) AS (r agtype
17171717
{"id": 1125899906842626, "label": "RELATED_TO", "end_id": 281474976710660, "start_id": 281474976710659, "properties": {"property1": "something", "property2": "else"}}::edge
17181718
(1 row)
17191719

1720+
--
1721+
-- Fix issue 1446: First MERGE does not see the second MERGE's changes
1722+
--
1723+
-- When chained MERGEs appear (MATCH ... MERGE ... MERGE ...), the first
1724+
-- (non-terminal) MERGE returned rows one at a time, so the second MERGE's
1725+
-- lateral join was materialized before the first finished all iterations.
1726+
-- This caused duplicate nodes. The fix makes non-terminal MERGE eager:
1727+
-- it processes ALL input rows before returning any.
1728+
--
1729+
SELECT * FROM create_graph('issue_1446');
1730+
NOTICE: graph "issue_1446" has been created
1731+
create_graph
1732+
--------------
1733+
1734+
(1 row)
1735+
1736+
-- Reporter's exact setup: two initial nodes
1737+
SELECT * FROM cypher('issue_1446', $$ CREATE (:A), (:C) $$) AS (a agtype);
1738+
a
1739+
---
1740+
(0 rows)
1741+
1742+
-- Reporter's exact reproduction case: two chained MERGEs
1743+
-- Without fix: C is created multiple times (once per MATCH row) because the
1744+
-- second MERGE's lateral join materializes before the first MERGE finishes.
1745+
-- With fix: returns 2 rows, C is found and reused by the second MERGE.
1746+
SELECT * FROM cypher('issue_1446', $$
1747+
MATCH (x)
1748+
MERGE (x)-[:r]->(:t)
1749+
MERGE (:C)-[:r]->(:t)
1750+
RETURN x
1751+
$$) AS (a agtype);
1752+
a
1753+
------------------------------------------------------------------
1754+
{"id": 844424930131969, "label": "A", "properties": {}}::vertex
1755+
{"id": 1125899906842625, "label": "C", "properties": {}}::vertex
1756+
(2 rows)
1757+
1758+
-- Verify: A(1), C(1), t(2) = 4 nodes, 2 edges
1759+
SELECT * FROM cypher('issue_1446', $$
1760+
MATCH (n)
1761+
RETURN labels(n) AS label, count(*) AS cnt
1762+
ORDER BY label
1763+
$$) AS (label agtype, cnt agtype);
1764+
label | cnt
1765+
-------+-----
1766+
["A"] | 1
1767+
["C"] | 1
1768+
["t"] | 2
1769+
(3 rows)
1770+
1771+
SELECT * FROM cypher('issue_1446', $$
1772+
MATCH ()-[e]->()
1773+
RETURN count(*) AS edge_count
1774+
$$) AS (edge_count agtype);
1775+
edge_count
1776+
------------
1777+
2
1778+
(1 row)
1779+
1780+
-- Test with 3 initial nodes: ensures eager buffering works for larger sets
1781+
SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
1782+
a
1783+
---
1784+
(0 rows)
1785+
1786+
SELECT * FROM cypher('issue_1446', $$ CREATE (:X), (:Y), (:Z) $$) AS (a agtype);
1787+
a
1788+
---
1789+
(0 rows)
1790+
1791+
SELECT * FROM cypher('issue_1446', $$
1792+
MATCH (n)
1793+
MERGE (n)-[:link]->(:shared)
1794+
MERGE (:hub)-[:link]->(:shared)
1795+
RETURN n
1796+
$$) AS (a agtype);
1797+
a
1798+
------------------------------------------------------------------
1799+
{"id": 1970324836974593, "label": "X", "properties": {}}::vertex
1800+
{"id": 2251799813685249, "label": "Y", "properties": {}}::vertex
1801+
{"id": 2533274790395905, "label": "Z", "properties": {}}::vertex
1802+
(3 rows)
1803+
1804+
-- Without fix: hub is created 3 times (once per MATCH row).
1805+
-- With fix: hub(1), shared(4), X(1), Y(1), Z(1) = 8 nodes, 4 edges
1806+
-- (3 n->shared edges + 1 hub->shared edge; hub reused for rows 2 & 3)
1807+
SELECT * FROM cypher('issue_1446', $$
1808+
MATCH (n)
1809+
RETURN labels(n) AS label, count(*) AS cnt
1810+
ORDER BY label
1811+
$$) AS (label agtype, cnt agtype);
1812+
label | cnt
1813+
------------+-----
1814+
["X"] | 1
1815+
["Y"] | 1
1816+
["Z"] | 1
1817+
["hub"] | 1
1818+
["shared"] | 4
1819+
(5 rows)
1820+
1821+
SELECT * FROM cypher('issue_1446', $$
1822+
MATCH ()-[e]->()
1823+
RETURN count(*) AS edge_count
1824+
$$) AS (edge_count agtype);
1825+
edge_count
1826+
------------
1827+
4
1828+
(1 row)
1829+
17201830
--
17211831
-- clean up graphs
17221832
--
@@ -1735,6 +1845,11 @@ SELECT * FROM cypher('issue_1709', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype
17351845
---
17361846
(0 rows)
17371847

1848+
SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
1849+
a
1850+
---
1851+
(0 rows)
1852+
17381853
--
17391854
-- delete graphs
17401855
--
@@ -1812,6 +1927,26 @@ NOTICE: graph "issue_1709" has been dropped
18121927

18131928
(1 row)
18141929

1930+
SELECT drop_graph('issue_1446', true);
1931+
NOTICE: drop cascades to 12 other objects
1932+
DETAIL: drop cascades to table issue_1446._ag_label_vertex
1933+
drop cascades to table issue_1446._ag_label_edge
1934+
drop cascades to table issue_1446."A"
1935+
drop cascades to table issue_1446."C"
1936+
drop cascades to table issue_1446.r
1937+
drop cascades to table issue_1446.t
1938+
drop cascades to table issue_1446."X"
1939+
drop cascades to table issue_1446."Y"
1940+
drop cascades to table issue_1446."Z"
1941+
drop cascades to table issue_1446.link
1942+
drop cascades to table issue_1446.shared
1943+
drop cascades to table issue_1446.hub
1944+
NOTICE: graph "issue_1446" has been dropped
1945+
drop_graph
1946+
------------
1947+
1948+
(1 row)
1949+
18151950
--
18161951
-- End
18171952
--

regress/sql/cypher_merge.sql

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,12 +785,68 @@ SELECT * FROM cypher('issue_1907', $$ MERGE (a {name: 'Test Node A'})-[r:RELATED
785785
-- should return properties added
786786
SELECT * FROM cypher('issue_1907', $$ MATCH ()-[r]->() RETURN r $$) AS (r agtype);
787787

788+
--
789+
-- Fix issue 1446: First MERGE does not see the second MERGE's changes
790+
--
791+
-- When chained MERGEs appear (MATCH ... MERGE ... MERGE ...), the first
792+
-- (non-terminal) MERGE returned rows one at a time, so the second MERGE's
793+
-- lateral join was materialized before the first finished all iterations.
794+
-- This caused duplicate nodes. The fix makes non-terminal MERGE eager:
795+
-- it processes ALL input rows before returning any.
796+
--
797+
SELECT * FROM create_graph('issue_1446');
798+
-- Reporter's exact setup: two initial nodes
799+
SELECT * FROM cypher('issue_1446', $$ CREATE (:A), (:C) $$) AS (a agtype);
800+
-- Reporter's exact reproduction case: two chained MERGEs
801+
-- Without fix: C is created multiple times (once per MATCH row) because the
802+
-- second MERGE's lateral join materializes before the first MERGE finishes.
803+
-- With fix: returns 2 rows, C is found and reused by the second MERGE.
804+
SELECT * FROM cypher('issue_1446', $$
805+
MATCH (x)
806+
MERGE (x)-[:r]->(:t)
807+
MERGE (:C)-[:r]->(:t)
808+
RETURN x
809+
$$) AS (a agtype);
810+
-- Verify: A(1), C(1), t(2) = 4 nodes, 2 edges
811+
SELECT * FROM cypher('issue_1446', $$
812+
MATCH (n)
813+
RETURN labels(n) AS label, count(*) AS cnt
814+
ORDER BY label
815+
$$) AS (label agtype, cnt agtype);
816+
SELECT * FROM cypher('issue_1446', $$
817+
MATCH ()-[e]->()
818+
RETURN count(*) AS edge_count
819+
$$) AS (edge_count agtype);
820+
821+
-- Test with 3 initial nodes: ensures eager buffering works for larger sets
822+
SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
823+
SELECT * FROM cypher('issue_1446', $$ CREATE (:X), (:Y), (:Z) $$) AS (a agtype);
824+
SELECT * FROM cypher('issue_1446', $$
825+
MATCH (n)
826+
MERGE (n)-[:link]->(:shared)
827+
MERGE (:hub)-[:link]->(:shared)
828+
RETURN n
829+
$$) AS (a agtype);
830+
-- Without fix: hub is created 3 times (once per MATCH row).
831+
-- With fix: hub(1), shared(4), X(1), Y(1), Z(1) = 8 nodes, 4 edges
832+
-- (3 n->shared edges + 1 hub->shared edge; hub reused for rows 2 & 3)
833+
SELECT * FROM cypher('issue_1446', $$
834+
MATCH (n)
835+
RETURN labels(n) AS label, count(*) AS cnt
836+
ORDER BY label
837+
$$) AS (label agtype, cnt agtype);
838+
SELECT * FROM cypher('issue_1446', $$
839+
MATCH ()-[e]->()
840+
RETURN count(*) AS edge_count
841+
$$) AS (edge_count agtype);
842+
788843
--
789844
-- clean up graphs
790845
--
791846
SELECT * FROM cypher('cypher_merge', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
792847
SELECT * FROM cypher('issue_1630', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
793848
SELECT * FROM cypher('issue_1709', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
849+
SELECT * FROM cypher('issue_1446', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype);
794850

795851
--
796852
-- delete graphs
@@ -800,6 +856,7 @@ SELECT drop_graph('cypher_merge', true);
800856
SELECT drop_graph('issue_1630', true);
801857
SELECT drop_graph('issue_1691', true);
802858
SELECT drop_graph('issue_1709', true);
859+
SELECT drop_graph('issue_1446', true);
803860

804861
--
805862
-- End

0 commit comments

Comments
 (0)