From d651adc5a1779d859588156ba0892452332e32f1 Mon Sep 17 00:00:00 2001 From: Tender Wang Date: Sun, 15 Nov 2026 09:36:58 +0800 Subject: [PATCH v2] Fix nullingrels for expressions above MERGE joins When MERGE requires an outer join, Vars used above that join must be marked nullable if they belong to one of its nullable sides. transform_MERGE_to_join() handled this asymmetrically. For left and full joins, it marked source Vars in the merge join condition, action expressions, and RETURNING list. For right and full joins, however, it only marked target Vars in the query target list. Target Vars in action expressions and the RETURNING list remained unmarked. Such Vars can later be added to the processed target list for evaluation by ModifyTable. This can leave an upper plan node with a Var whose varnullingrels do not match the corresponding Var produced by its child, leading to a "wrong varnullingrels" error during setrefs processing. Refactor the code to first collect the RT indexes for all nullable sides of the MERGE join. Use that set to mark Vars in the query target list, action expressions, and RETURNING list. For left and full joins, also apply it to the copy of the merge join condition evaluated above the join for NOT MATCHED BY SOURCE processing. Leave the original join condition in the JoinExpr unchanged. --- src/backend/optimizer/prep/prepjointree.c | 62 +++++++++++------------ src/test/regress/expected/merge.out | 16 ++++++ src/test/regress/sql/merge.sql | 21 ++++++++ 3 files changed, 67 insertions(+), 32 deletions(-) diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 0a9dd57b852..7db2e1e256c 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -214,6 +214,7 @@ transform_MERGE_to_join(Query *parse) FromExpr *target; Node *source; int sourcerti; + Relids nullable_relids = NULL; if (parse->commandType != CMD_MERGE) return; @@ -323,23 +324,25 @@ transform_MERGE_to_join(Query *parse) parse->jointree->quals = NULL; /* - * If necessary, mark parse->targetlist entries that refer to the target - * as nullable by the join. Normally the targetlist will be empty for a - * MERGE, but if the target is a trigger-updatable view, it will contain a - * whole-row Var referring to the expanded view query. + * Collect the RT indexes of relations on nullable sides of the join. */ - if (parse->targetList != NIL && - (jointype == JOIN_RIGHT || jointype == JOIN_FULL)) - parse->targetList = (List *) - add_nulling_relids((Node *) parse->targetList, - bms_make_singleton(parse->mergeTargetRelation), - bms_make_singleton(joinrti)); + if (jointype == JOIN_RIGHT || jointype == JOIN_FULL) + nullable_relids = + bms_add_member(nullable_relids, parse->mergeTargetRelation); + if (jointype == JOIN_LEFT || jointype == JOIN_FULL) + nullable_relids = + bms_join(nullable_relids, + get_relids_in_jointree(source, true, true)); /* - * If the source relation is on the outer side of the join, mark any - * source relation Vars in the join condition, actions, and RETURNING list - * as nullable by the join. These Vars will be added to the targetlist by - * preprocess_targetlist(), so it's important to mark them correctly here. + * Mark Vars used above the join as nullable by the join. This includes + * Vars in the query targetlist, action expressions, and RETURNING list. + * Vars from the latter expressions will be added to the targetlist by + * preprocess_targetlist(), so they must be marked correctly here. + * + * Normally the query targetlist is empty for MERGE, but for a + * trigger-updatable view it contains a whole-row Var referring to the + * expanded view query. * * It might seem that this is not necessary for Vars in the join * condition, since it is inside the join, but it is also needed above the @@ -348,38 +351,33 @@ transform_MERGE_to_join(Query *parse) * creates a modified copy of the join condition, for use above the join, * without modifying the original join condition, inside the join. */ - if (jointype == JOIN_LEFT || jointype == JOIN_FULL) + if (nullable_relids != NULL) { - Relids sourcerelids; - - /* - * If the source relation is a join, we must mark any Vars referring - * to relations under the join as well as the join relation itself. - */ - sourcerelids = get_relids_in_jointree(source, true, true); + Relids nullingrel = bms_make_singleton(joinrti); - parse->mergeJoinCondition = - add_nulling_relids(parse->mergeJoinCondition, - sourcerelids, - bms_make_singleton(joinrti)); + parse->targetList = (List *) + add_nulling_relids((Node *) parse->targetList, + nullable_relids, nullingrel); foreach_node(MergeAction, action, parse->mergeActionList) { action->qual = add_nulling_relids(action->qual, - sourcerelids, - bms_make_singleton(joinrti)); + nullable_relids, nullingrel); action->targetList = (List *) add_nulling_relids((Node *) action->targetList, - sourcerelids, - bms_make_singleton(joinrti)); + nullable_relids, nullingrel); } parse->returningList = (List *) add_nulling_relids((Node *) parse->returningList, - sourcerelids, - bms_make_singleton(joinrti)); + nullable_relids, nullingrel); + + if (jointype == JOIN_LEFT || jointype == JOIN_FULL) + parse->mergeJoinCondition = + add_nulling_relids(parse->mergeJoinCondition, + nullable_relids, nullingrel); } /* diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out index 905c3fe8cb1..3a7202a3605 100644 --- a/src/test/regress/expected/merge.out +++ b/src/test/regress/expected/merge.out @@ -2921,6 +2921,22 @@ SELECT * FROM bug18634t; DROP TABLE bug18634t CASCADE; NOTICE: drop cascades to view bug18634v DROP TABLE bug18634s, bug18634s2; +-- Test nullingrels for target Vars above the right join generated for MERGE +-- into a trigger-updatable view with a join source. +CREATE TABLE src (a int, b int); +CREATE TABLE tgt (a int, b int); +CREATE VIEW tgt_v AS SELECT * FROM tgt; +CREATE FUNCTION tgt_v_ins_fn() RETURNS trigger + LANGUAGE plpgsql AS $$ BEGIN RETURN new; END $$; +CREATE TRIGGER tgt_v_ins_trig INSTEAD OF INSERT ON tgt_v + FOR EACH ROW EXECUTE FUNCTION tgt_v_ins_fn(); +MERGE INTO tgt_v t + USING (src s JOIN src s2 ON s2.b = s.b) ON t.b = s.b AND t.b = 0 + WHEN MATCHED AND t.b = 0 THEN DO NOTHING + WHEN NOT MATCHED THEN INSERT VALUES (s.a, s.b); +DROP VIEW tgt_v; +DROP FUNCTION tgt_v_ins_fn(); +DROP TABLE src, tgt; -- prepare RESET SESSION AUTHORIZATION; -- try a system catalog diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql index 37aef4de68a..91a8428e979 100644 --- a/src/test/regress/sql/merge.sql +++ b/src/test/regress/sql/merge.sql @@ -1882,6 +1882,27 @@ SELECT * FROM bug18634t; DROP TABLE bug18634t CASCADE; DROP TABLE bug18634s, bug18634s2; +-- Test nullingrels for target Vars above the right join generated for MERGE +-- into a trigger-updatable view with a join source. +CREATE TABLE src (a int, b int); +CREATE TABLE tgt (a int, b int); +CREATE VIEW tgt_v AS SELECT * FROM tgt; + +CREATE FUNCTION tgt_v_ins_fn() RETURNS trigger + LANGUAGE plpgsql AS $$ BEGIN RETURN new; END $$; + +CREATE TRIGGER tgt_v_ins_trig INSTEAD OF INSERT ON tgt_v + FOR EACH ROW EXECUTE FUNCTION tgt_v_ins_fn(); + +MERGE INTO tgt_v t + USING (src s JOIN src s2 ON s2.b = s.b) ON t.b = s.b AND t.b = 0 + WHEN MATCHED AND t.b = 0 THEN DO NOTHING + WHEN NOT MATCHED THEN INSERT VALUES (s.a, s.b); + +DROP VIEW tgt_v; +DROP FUNCTION tgt_v_ins_fn(); +DROP TABLE src, tgt; + -- prepare RESET SESSION AUTHORIZATION; -- 2.43.0