From bc2f84e0718a9f1f2b2b99df8bd16b7ec3f1bd68 Mon Sep 17 00:00:00 2001 From: Tender Wang Date: Thu, 5 Nov 2026 11:27:15 +0800 Subject: [PATCH] Fix source visibility in MERGE NOT MATCHED BY SOURCE actions. When the MERGE source is an unaliased join, hiding only its top-level RTE leaves the input relations visible by name. This allows NOT MATCHED BY SOURCE actions to reference source columns in their conditions and UPDATE expressions, although such references should be rejected. Temporarily restrict the namespace for each action according to its match kind, and restore it afterwards, following the pattern used by transformJoinOnClause(). Use the complete source namespace to preserve the visibility rules for join inputs and aliases. --- src/backend/parser/parse_merge.c | 122 ++++++---------------------- src/test/regress/expected/merge.out | 33 ++++++++ src/test/regress/sql/merge.sql | 28 +++++++ 3 files changed, 88 insertions(+), 95 deletions(-) diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c index 0a70d48fd4c..ec7f357801c 100644 --- a/src/backend/parser/parse_merge.c +++ b/src/backend/parser/parse_merge.c @@ -28,77 +28,6 @@ #include "parser/parsetree.h" #include "utils/rel.h" -static void setNamespaceForMergeWhen(ParseState *pstate, - MergeWhenClause *mergeWhenClause, - Index targetRTI, - Index sourceRTI); -static void setNamespaceVisibilityForRTE(List *namespace, RangeTblEntry *rte, - bool rel_visible, - bool cols_visible); - -/* - * Make appropriate changes to the namespace visibility while transforming - * individual action's quals and targetlist expressions. In particular, for - * INSERT actions we must only see the source relation (since INSERT action is - * invoked for NOT MATCHED [BY TARGET] tuples and hence there is no target - * tuple to deal with). On the other hand, UPDATE and DELETE actions can see - * both source and target relations, unless invoked for NOT MATCHED BY SOURCE. - * - * Also, since the internal join node can hide the source and target - * relations, we must explicitly make the respective relation as visible so - * that columns can be referenced unqualified from these relations. - */ -static void -setNamespaceForMergeWhen(ParseState *pstate, MergeWhenClause *mergeWhenClause, - Index targetRTI, Index sourceRTI) -{ - RangeTblEntry *targetRelRTE, - *sourceRelRTE; - - targetRelRTE = rt_fetch(targetRTI, pstate->p_rtable); - sourceRelRTE = rt_fetch(sourceRTI, pstate->p_rtable); - - if (mergeWhenClause->matchKind == MERGE_WHEN_MATCHED) - { - Assert(mergeWhenClause->commandType == CMD_UPDATE || - mergeWhenClause->commandType == CMD_DELETE || - mergeWhenClause->commandType == CMD_NOTHING); - - /* MATCHED actions can see both target and source relations. */ - setNamespaceVisibilityForRTE(pstate->p_namespace, - targetRelRTE, true, true); - setNamespaceVisibilityForRTE(pstate->p_namespace, - sourceRelRTE, true, true); - } - else if (mergeWhenClause->matchKind == MERGE_WHEN_NOT_MATCHED_BY_SOURCE) - { - /* - * NOT MATCHED BY SOURCE actions can see the target relation, but they - * can't see the source relation. - */ - Assert(mergeWhenClause->commandType == CMD_UPDATE || - mergeWhenClause->commandType == CMD_DELETE || - mergeWhenClause->commandType == CMD_NOTHING); - setNamespaceVisibilityForRTE(pstate->p_namespace, - targetRelRTE, true, true); - setNamespaceVisibilityForRTE(pstate->p_namespace, - sourceRelRTE, false, false); - } - else /* MERGE_WHEN_NOT_MATCHED_BY_TARGET */ - { - /* - * NOT MATCHED [BY TARGET] actions can't see target relation, but they - * can see source relation. - */ - Assert(mergeWhenClause->commandType == CMD_INSERT || - mergeWhenClause->commandType == CMD_NOTHING); - setNamespaceVisibilityForRTE(pstate->p_namespace, - targetRelRTE, false, false); - setNamespaceVisibilityForRTE(pstate->p_namespace, - sourceRelRTE, true, true); - } -} - /* * transformMergeStmt - * transforms a MERGE statement @@ -112,6 +41,7 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) bool is_terminal[NUM_MERGE_MATCH_KINDS]; Index sourceRTI; List *mergeActionList; + List *sourceNamespace; ParseNamespaceItem *nsitem; /* There can't be any outer WITH to worry about */ @@ -208,6 +138,8 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) transformFromClause(pstate, list_make1(stmt->sourceRelation)); sourceRTI = list_length(pstate->p_rtable); + /* Save the source namespace before adding the target below. */ + sourceNamespace = list_copy(pstate->p_namespace); nsitem = GetNSItemByRangeTablePosn(pstate, sourceRTI, 0); /* @@ -269,18 +201,34 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) { MergeWhenClause *mergeWhenClause = lfirst_node(MergeWhenClause, l); MergeAction *action; + List *save_namespace; action = makeNode(MergeAction); action->commandType = mergeWhenClause->commandType; action->matchKind = mergeWhenClause->matchKind; /* - * Set namespace for the specific action. This must be done before - * analyzing the WHEN quals and the action targetlist. + * Temporarily restrict the namespace to the relations visible to this + * action. Use the complete source namespace, rather than just its + * top-level RTE, so that inputs of an unaliased source join are + * handled too. Preserve the visibility flags established by + * transformFromClause(). */ - setNamespaceForMergeWhen(pstate, mergeWhenClause, - qry->resultRelation, - sourceRTI); + save_namespace = pstate->p_namespace; + switch (mergeWhenClause->matchKind) + { + case MERGE_WHEN_MATCHED: + /* Both source and target are already visible. */ + break; + case MERGE_WHEN_NOT_MATCHED_BY_SOURCE: + pstate->p_namespace = list_make1(pstate->p_target_nsitem); + break; + case MERGE_WHEN_NOT_MATCHED_BY_TARGET: + pstate->p_namespace = sourceNamespace; + break; + default: + elog(ERROR, "unknown match kind in MERGE WHEN clause"); + } /* * Transform the WHEN condition. @@ -393,6 +341,9 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) elog(ERROR, "unknown action in MERGE WHEN clause"); } + /* Restore the namespace for subsequent actions. */ + pstate->p_namespace = save_namespace; + mergeActionList = lappend(mergeActionList, action); } @@ -406,22 +357,3 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt) return qry; } -static void -setNamespaceVisibilityForRTE(List *namespace, RangeTblEntry *rte, - bool rel_visible, - bool cols_visible) -{ - ListCell *lc; - - foreach(lc, namespace) - { - ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc); - - if (nsitem->p_rte == rte) - { - nsitem->p_rel_visible = rel_visible; - nsitem->p_cols_visible = cols_visible; - break; - } - } -} diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out index 9cb1d87066a..fc34c2ac83a 100644 --- a/src/test/regress/expected/merge.out +++ b/src/test/regress/expected/merge.out @@ -2466,6 +2466,39 @@ WHEN NOT MATCHED THEN INSERT VALUES (sid2, delta, sval) WHEN MATCHED THEN DELETE; +-- NOT MATCHED BY SOURCE must not see either input of a source join +EXPLAIN (COSTS OFF) +MERGE INTO cj_target t +USING cj_source1 s1 CROSS JOIN cj_source2 s2 +ON t.tid = s1.sid1 +WHEN NOT MATCHED BY SOURCE THEN + UPDATE SET balance = s1.delta; +ERROR: invalid reference to FROM-clause entry for table "s1" +LINE 6: UPDATE SET balance = s1.delta; + ^ +DETAIL: There is an entry for table "s1", but it cannot be referenced from this part of the query. +EXPLAIN (COSTS OFF) +MERGE INTO cj_target t +USING cj_source1 s1 CROSS JOIN cj_source2 s2 +ON t.tid = s1.sid1 +WHEN NOT MATCHED BY SOURCE AND s2.sval IS NULL THEN + DELETE; +ERROR: invalid reference to FROM-clause entry for table "s2" +LINE 5: WHEN NOT MATCHED BY SOURCE AND s2.sval IS NULL THEN + ^ +DETAIL: There is an entry for table "s2", but it cannot be referenced from this part of the query. +-- A later action must still be able to see the source join inputs +BEGIN; +MERGE INTO cj_target t +USING cj_source1 s1 JOIN cj_source2 s2 ON s1.sid1 = s2.sid2 +ON t.tid = s1.sid1 +WHEN NOT MATCHED BY SOURCE THEN + UPDATE SET balance = 0 +WHEN MATCHED AND s1.scat = 10 THEN + UPDATE SET val = s2.sval +WHEN NOT MATCHED THEN + INSERT VALUES (s1.sid1, s1.delta, s2.sval); +ROLLBACK; -- some simple expressions in INSERT targetlist MERGE INTO cj_target t USING cj_source2 s2 diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql index 2660b19f238..39913eb21c0 100644 --- a/src/test/regress/sql/merge.sql +++ b/src/test/regress/sql/merge.sql @@ -1544,6 +1544,34 @@ WHEN NOT MATCHED THEN WHEN MATCHED THEN DELETE; +-- NOT MATCHED BY SOURCE must not see either input of a source join +EXPLAIN (COSTS OFF) +MERGE INTO cj_target t +USING cj_source1 s1 CROSS JOIN cj_source2 s2 +ON t.tid = s1.sid1 +WHEN NOT MATCHED BY SOURCE THEN + UPDATE SET balance = s1.delta; + +EXPLAIN (COSTS OFF) +MERGE INTO cj_target t +USING cj_source1 s1 CROSS JOIN cj_source2 s2 +ON t.tid = s1.sid1 +WHEN NOT MATCHED BY SOURCE AND s2.sval IS NULL THEN + DELETE; + +-- A later action must still be able to see the source join inputs +BEGIN; +MERGE INTO cj_target t +USING cj_source1 s1 JOIN cj_source2 s2 ON s1.sid1 = s2.sid2 +ON t.tid = s1.sid1 +WHEN NOT MATCHED BY SOURCE THEN + UPDATE SET balance = 0 +WHEN MATCHED AND s1.scat = 10 THEN + UPDATE SET val = s2.sval +WHEN NOT MATCHED THEN + INSERT VALUES (s1.sid1, s1.delta, s2.sval); +ROLLBACK; + -- some simple expressions in INSERT targetlist MERGE INTO cj_target t USING cj_source2 s2 -- 2.43.0