From bf286cb68fa41f4a7d644538b00a46c6b12893cc Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Sun, 23 Aug 2026 21:08:32 +0800 Subject: [PATCH] fix: guard against NULL subplan in full-path MERGE sub-case 1 (#2536) When MERGE is the first clause followed by WITH (Case 3, sub-case 1), the code assumes the child execution node is always a SubqueryScanState and dereferences sss->subplan to obtain the tuple descriptor for the remade scan slot. When the MERGE path pattern produces no base-relation scan (e.g. empty graph), the planner emits a plain Result plan instead of a SubqueryScan, the cast-to-SubqueryScanState reads through the wrong struct layout, sss->subplan is NULL, and ExecGetResultType(NULL) segfaults. Fix: check IsA(lefttree, SubqueryScanState) before using SubqueryScanState internals. When the child is a SubqueryScan the original code path (remake child scan slot via sss->ss + ExecGetResultType(sss->subplan)) is preserved without regression. When the child is a Result plan, use the generic PlanState API (ExecGetResultType(lefttree)) and store the remade slot in the MERGE node's own ScanState. Fixes #2536. --- regress/expected/expr.out | 35 ++++++++ regress/sql/expr.sql | 15 ++++ src/backend/executor/cypher_merge.c | 123 ++++++++++++++++++---------- 3 files changed, 130 insertions(+), 43 deletions(-) diff --git a/regress/expected/expr.out b/regress/expected/expr.out index 806a6f65c..8f467d746 100644 --- a/regress/expected/expr.out +++ b/regress/expected/expr.out @@ -10711,3 +10711,38 @@ NOTICE: graph "issue_2391" has been dropped -- -- End of tests -- +-- Issue #2536: Full-path MERGE with WITH followed by WHERE +-- on a non-existing pattern (empty graph). The child plan is a +-- Result node, not a SubqueryScan, and the code must not assume +-- SubqueryScanState internals when remaking the scan tuple slot. +SELECT * FROM create_graph('issue_2536'); +NOTICE: graph "issue_2536" has been created + create_graph +-------------- + +(1 row) + +SELECT * FROM cypher('issue_2536', $$ + MERGE p = (:a)-[:r]->(n) + WITH 1 AS y + WHERE ('a' STARTS WITH 'a') + RETURN y +$$) AS (y agtype); + y +--- + 1 +(1 row) + +SELECT * FROM drop_graph('issue_2536', true); +NOTICE: drop cascades to 4 other objects +DETAIL: drop cascades to table issue_2536._ag_label_vertex +drop cascades to table issue_2536._ag_label_edge +drop cascades to table issue_2536.a +drop cascades to table issue_2536.r +NOTICE: graph "issue_2536" has been dropped + drop_graph +------------ + +(1 row) + +-- End of tests diff --git a/regress/sql/expr.sql b/regress/sql/expr.sql index d4d900a1c..951384e80 100644 --- a/regress/sql/expr.sql +++ b/regress/sql/expr.sql @@ -4203,3 +4203,18 @@ SELECT * FROM drop_graph('issue_2391', true); -- -- End of tests -- + +-- Issue #2536: Full-path MERGE with WITH followed by WHERE +-- on a non-existing pattern (empty graph). The child plan is a +-- Result node, not a SubqueryScan, and the code must not assume +-- SubqueryScanState internals when remaking the scan tuple slot. +SELECT * FROM create_graph('issue_2536'); +SELECT * FROM cypher('issue_2536', $$ + MERGE p = (:a)-[:r]->(n) + WITH 1 AS y + WHERE ('a' STARTS WITH 'a') + RETURN y +$$) AS (y agtype); +SELECT * FROM drop_graph('issue_2536', true); + +-- End of tests diff --git a/src/backend/executor/cypher_merge.c b/src/backend/executor/cypher_merge.c index 9c52073c2..8ea7a3219 100644 --- a/src/backend/executor/cypher_merge.c +++ b/src/backend/executor/cypher_merge.c @@ -960,15 +960,7 @@ static TupleTableSlot *exec_cypher_merge(CustomScanState *node) * So we will need to create a TupleTableSlot and populate with the * information from the newly created path that the query needs. */ - SubqueryScanState *sss = NULL; econtext = node->ss.ps.ps_ExprContext; - sss = (SubqueryScanState *)node->ss.ps.lefttree; - - /* - * Our child execution node is always a subquery. If not there - * is an issue. - */ - Assert(IsA(sss, SubqueryScanState)); /* * found_a_path should only be set to true if MERGE is following @@ -985,51 +977,96 @@ static TupleTableSlot *exec_cypher_merge(CustomScanState *node) Assert(css->created_new_path == false); /* - * Postgres cleared the child tuple table slot, we need to remake - * it. + * Postgres cleared the child tuple table slot, we need to remake + * it. The child may be a SubqueryScan (when the MERGE path + * pattern involves a scan) or a plain Result plan (when the MERGE + * pattern produces no base-relation scan, e.g. on an empty graph). + * Use the correct API for each case. */ - ExecInitScanTupleSlot(estate, &sss->ss, - ExecGetResultType(sss->subplan), - &TTSOpsVirtual); + if (IsA(node->ss.ps.lefttree, SubqueryScanState)) + { + SubqueryScanState *sss = (SubqueryScanState *)node->ss.ps.lefttree; - /* setup the scantuple that the process_path needs */ - econtext->ecxt_scantuple = sss->ss.ss_ScanTupleSlot; + ExecInitScanTupleSlot(estate, &sss->ss, + ExecGetResultType(sss->subplan), + &TTSOpsVirtual); + econtext->ecxt_scantuple = sss->ss.ss_ScanTupleSlot; - /* - * Initialize the scan tuple slot as all-null before process_path - * populates it with the created entities. This ensures the slot - * is properly set up for apply_update_list. - */ - mark_tts_isnull(econtext->ecxt_scantuple); + mark_tts_isnull(econtext->ecxt_scantuple); - /* create the path */ - process_path(css, NULL, true); + /* create the path */ + process_path(css, NULL, true); - /* mark the slot as valid so tts_nvalid reflects natts */ - mark_scan_slot_valid(econtext->ecxt_scantuple); + mark_scan_slot_valid(econtext->ecxt_scantuple); - /* ON CREATE SET: path was just created */ - if (css->on_create_set_info) - apply_update_list(&css->css, css->on_create_set_info); + /* ON CREATE SET: path was just created */ + if (css->on_create_set_info) + apply_update_list(&css->css, css->on_create_set_info); - /* mark the create_new_path flag to true. */ - css->created_new_path = true; + /* mark the create_new_path flag to true. */ + css->created_new_path = true; - /* - * make the subquery's projection scan slot be the tuple table we - * created and run the projection logic. - */ - sss->ss.ps.ps_ProjInfo->pi_exprContext->ecxt_scantuple = - econtext->ecxt_scantuple; + /* + * make the subquery's projection scan slot be the tuple table + * we created and run the projection logic. + */ + sss->ss.ps.ps_ProjInfo->pi_exprContext->ecxt_scantuple = + econtext->ecxt_scantuple; - /* assign this to be our scantuple */ - econtext->ecxt_scantuple = ExecProject(node->ss.ps.lefttree->ps_ProjInfo); + /* assign this to be our scantuple */ + econtext->ecxt_scantuple = + ExecProject(node->ss.ps.lefttree->ps_ProjInfo); - /* - * run the merge's projection logic and pass to its parent - * execution node - */ - return ExecProject(node->ss.ps.ps_ProjInfo); + /* + * run the merge's projection logic and pass to its parent + * execution node + */ + return ExecProject(node->ss.ps.ps_ProjInfo); + } + else + { + /* + * The child is a plain Result plan (no base-relation scan). + * Use the generic PlanState API — SubqueryScanState internals + * would read through the wrong struct layout and crash + * (NULL subplan -> ExecGetResultType(NULL)). + */ + ExecInitScanTupleSlot(estate, &node->ss, + ExecGetResultType(node->ss.ps.lefttree), + &TTSOpsVirtual); + econtext->ecxt_scantuple = node->ss.ss_ScanTupleSlot; + + mark_tts_isnull(econtext->ecxt_scantuple); + + /* create the path */ + process_path(css, NULL, true); + + mark_scan_slot_valid(econtext->ecxt_scantuple); + + /* ON CREATE SET: path was just created */ + if (css->on_create_set_info) + apply_update_list(&css->css, css->on_create_set_info); + + /* mark the create_new_path flag to true. */ + css->created_new_path = true; + + /* + * make the subquery's projection scan slot be the tuple table + * we created and run the projection logic. + */ + node->ss.ps.lefttree->ps_ProjInfo->pi_exprContext->ecxt_scantuple = + econtext->ecxt_scantuple; + + /* assign this to be our scantuple */ + econtext->ecxt_scantuple = + ExecProject(node->ss.ps.lefttree->ps_ProjInfo); + + /* + * run the merge's projection logic and pass to its parent + * execution node + */ + return ExecProject(node->ss.ps.ps_ProjInfo); + } } } }