From c15997956f9ec07f65d774ef16612a33c9c387dc Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Mon, 24 Aug 2026 11:10:51 +0800 Subject: [PATCH 1/2] fix: guard against NULL scanTupleSlot in apply_update_list (#2537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DELETE matched relationship followed by full-path MERGE causes a PostgreSQL backend crash (signal 11, segfault) in apply_update_list at cypher_set.c:420. The scanTupleSlot in the expression context is NULL when MERGE's ON MATCH SET branch is reached after a DELETE has removed the matched edge, because the subtree's projection does not set ecxt_scantuple. Fix: check for NULL scanTupleSlot at the top of apply_update_list and return early — there is no data to update when the scan slot has not been initialized. Regression test: issue_2537 graph with the exact reproduction from the bug report (DELETE edge + CREATE + full-path MERGE). --- regress/expected/cypher_merge.out | 49 +++++++++++++++++++++++++++++++ regress/sql/cypher_merge.sql | 24 +++++++++++++++ repro2537.sql | 20 +++++++++++++ repro2537_fix.sql | 22 ++++++++++++++ src/backend/executor/cypher_set.c | 4 +++ 5 files changed, 119 insertions(+) create mode 100644 repro2537.sql create mode 100644 repro2537_fix.sql diff --git a/regress/expected/cypher_merge.out b/regress/expected/cypher_merge.out index ac08a971a..6c888bb00 100644 --- a/regress/expected/cypher_merge.out +++ b/regress/expected/cypher_merge.out @@ -2237,6 +2237,55 @@ SELECT * FROM cypher('merge_actions', $$ MATCH (n) DETACH DELETE n $$) AS (a agt --- (0 rows) +-- +-- Regression test for #2537: DELETE edge + full-path MERGE segfault +-- (apply_update_list scanTupleSlot NULL deref) +-- +SELECT * FROM create_graph('issue_2537'); +NOTICE: graph "issue_2537" has been created + create_graph +-------------- + +(1 row) + +SELECT * FROM cypher('issue_2537', $$ +CREATE (a:base {id: 1})-[r1:r {k0: false}]-> + (n:base {id: 2})-[r0:s {k5: 'x'}]->(b:base {id: 3}) +$$) AS (created agtype); + created +--------- +(0 rows) + +SELECT * FROM cypher('issue_2537', $$ +MATCH (a:base)-[r1:r]->(n:base)-[r0:s]->(b:base) +SET r0.k5 = 'J', r1.k0 = true +WITH range(0, 0) + [0] AS keep, r1 AS old +WHERE 0 IN keep +DELETE old +CREATE (:x) +MERGE p0 = ({id: 129})-[r2:r]->(n2 {id: 130})-[:s]->(:c {id: 131}) +RETURN 1 +$$) AS (value agtype); + value +------- + 1 +(1 row) + +SELECT * FROM drop_graph('issue_2537', true); +NOTICE: drop cascades to 7 other objects +DETAIL: drop cascades to table issue_2537._ag_label_vertex +drop cascades to table issue_2537._ag_label_edge +drop cascades to table issue_2537.base +drop cascades to table issue_2537.r +drop cascades to table issue_2537.s +drop cascades to table issue_2537.x +drop cascades to table issue_2537.c +NOTICE: graph "issue_2537" has been dropped + drop_graph +------------ + +(1 row) + -- -- delete graphs -- diff --git a/regress/sql/cypher_merge.sql b/regress/sql/cypher_merge.sql index 86b3e0235..86a21c239 100644 --- a/regress/sql/cypher_merge.sql +++ b/regress/sql/cypher_merge.sql @@ -1088,6 +1088,30 @@ $$) AS (src agtype, last agtype); -- cleanup SELECT * FROM cypher('merge_actions', $$ MATCH (n) DETACH DELETE n $$) AS (a agtype); +-- +-- Regression test for #2537: DELETE edge + full-path MERGE segfault +-- (apply_update_list scanTupleSlot NULL deref) +-- +SELECT * FROM create_graph('issue_2537'); + +SELECT * FROM cypher('issue_2537', $$ +CREATE (a:base {id: 1})-[r1:r {k0: false}]-> + (n:base {id: 2})-[r0:s {k5: 'x'}]->(b:base {id: 3}) +$$) AS (created agtype); + +SELECT * FROM cypher('issue_2537', $$ +MATCH (a:base)-[r1:r]->(n:base)-[r0:s]->(b:base) +SET r0.k5 = 'J', r1.k0 = true +WITH range(0, 0) + [0] AS keep, r1 AS old +WHERE 0 IN keep +DELETE old +CREATE (:x) +MERGE p0 = ({id: 129})-[r2:r]->(n2 {id: 130})-[:s]->(:c {id: 131}) +RETURN 1 +$$) AS (value agtype); + +SELECT * FROM drop_graph('issue_2537', true); + -- -- delete graphs -- diff --git a/repro2537.sql b/repro2537.sql new file mode 100644 index 000000000..8c41c66f2 --- /dev/null +++ b/repro2537.sql @@ -0,0 +1,20 @@ +LOAD 'age'; +SET search_path = ag_catalog, public; + +-- STEP 1: CREATE data +SELECT * FROM cypher('dbg_test', $$ +CREATE (a:base {id: 1})-[r1:r {k0: false}]-> + (n:base {id: 2})-[r0:s {k5: 'x'}]->(b:base {id: 3}) +$$) AS (created agtype); + +-- STEP 2: DELETE + MERGE (the bug) +SELECT * FROM cypher('dbg_test', $$ +MATCH (a:base)-[r1:r]->(n:base)-[r0:s]->(b:base) +SET r0.k5 = 'J', r1.k0 = true +WITH range(0, 0) + [0] AS keep, r1 AS old +WHERE 0 IN keep +DELETE old +CREATE (:x) +MERGE p0 = ({id: 129})-[r2:r]->(n2 {id: 130})-[:s]->(:c {id: 131}) +RETURN 1 +$$) AS (value agtype); \ No newline at end of file diff --git a/repro2537_fix.sql b/repro2537_fix.sql new file mode 100644 index 000000000..36b2fbfa0 --- /dev/null +++ b/repro2537_fix.sql @@ -0,0 +1,22 @@ +LOAD 'age'; +SET search_path = ag_catalog, public; + +SELECT create_graph('dbg_test2'); + +SELECT * FROM cypher('dbg_test2', $$ +CREATE (a:base {id: 1})-[r1:r {k0: false}]-> + (n:base {id: 2})-[r0:s {k5: 'x'}]->(b:base {id: 3}) +$$) AS (created agtype); + +SELECT * FROM cypher('dbg_test2', $$ +MATCH (a:base)-[r1:r]->(n:base)-[r0:s]->(b:base) +SET r0.k5 = 'J', r1.k0 = true +WITH range(0, 0) + [0] AS keep, r1 AS old +WHERE 0 IN keep +DELETE old +CREATE (:x) +MERGE p0 = ({id: 129})-[r2:r]->(n2 {id: 130})-[:s]->(:c {id: 131}) +RETURN 1 +$$) AS (value agtype); + +SELECT * FROM cypher('dbg_test2', $$ RETURN 1 $$) AS (v agtype); \ No newline at end of file diff --git a/src/backend/executor/cypher_set.c b/src/backend/executor/cypher_set.c index 9b6bf51c5..f3c6f3e68 100644 --- a/src/backend/executor/cypher_set.c +++ b/src/backend/executor/cypher_set.c @@ -416,6 +416,10 @@ void apply_update_list(CustomScanState *node, HTAB *index_cache = NULL; HASHCTL idx_hashctl; + /* if scanTupleSlot is NULL, there is no data to update */ + if (scanTupleSlot == NULL) + return; + /* allocate an array to hold the last update index of each 'entity' */ luindex = palloc0(sizeof(int) * scanTupleSlot->tts_nvalid); From 89cb3a8eae36199a6c31e7861047a42fa343e93a Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Mon, 24 Aug 2026 11:10:56 +0800 Subject: [PATCH 2/2] chore: remove local repro scripts --- repro2537.sql | 20 -------------------- repro2537_fix.sql | 22 ---------------------- 2 files changed, 42 deletions(-) delete mode 100644 repro2537.sql delete mode 100644 repro2537_fix.sql diff --git a/repro2537.sql b/repro2537.sql deleted file mode 100644 index 8c41c66f2..000000000 --- a/repro2537.sql +++ /dev/null @@ -1,20 +0,0 @@ -LOAD 'age'; -SET search_path = ag_catalog, public; - --- STEP 1: CREATE data -SELECT * FROM cypher('dbg_test', $$ -CREATE (a:base {id: 1})-[r1:r {k0: false}]-> - (n:base {id: 2})-[r0:s {k5: 'x'}]->(b:base {id: 3}) -$$) AS (created agtype); - --- STEP 2: DELETE + MERGE (the bug) -SELECT * FROM cypher('dbg_test', $$ -MATCH (a:base)-[r1:r]->(n:base)-[r0:s]->(b:base) -SET r0.k5 = 'J', r1.k0 = true -WITH range(0, 0) + [0] AS keep, r1 AS old -WHERE 0 IN keep -DELETE old -CREATE (:x) -MERGE p0 = ({id: 129})-[r2:r]->(n2 {id: 130})-[:s]->(:c {id: 131}) -RETURN 1 -$$) AS (value agtype); \ No newline at end of file diff --git a/repro2537_fix.sql b/repro2537_fix.sql deleted file mode 100644 index 36b2fbfa0..000000000 --- a/repro2537_fix.sql +++ /dev/null @@ -1,22 +0,0 @@ -LOAD 'age'; -SET search_path = ag_catalog, public; - -SELECT create_graph('dbg_test2'); - -SELECT * FROM cypher('dbg_test2', $$ -CREATE (a:base {id: 1})-[r1:r {k0: false}]-> - (n:base {id: 2})-[r0:s {k5: 'x'}]->(b:base {id: 3}) -$$) AS (created agtype); - -SELECT * FROM cypher('dbg_test2', $$ -MATCH (a:base)-[r1:r]->(n:base)-[r0:s]->(b:base) -SET r0.k5 = 'J', r1.k0 = true -WITH range(0, 0) + [0] AS keep, r1 AS old -WHERE 0 IN keep -DELETE old -CREATE (:x) -MERGE p0 = ({id: 129})-[r2:r]->(n2 {id: 130})-[:s]->(:c {id: 131}) -RETURN 1 -$$) AS (value agtype); - -SELECT * FROM cypher('dbg_test2', $$ RETURN 1 $$) AS (v agtype); \ No newline at end of file