From 7f56eaaf19cbcb61d73b02e91bc70e8270d2bcd4 Mon Sep 17 00:00:00 2001 From: Tender Wang Date: Fri, 13 Nov 2026 21:01:27 +0800 Subject: [PATCH] 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 are later added to the processed target list for evaluation by ModifyTable. This can make an upper plan node contain a target 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 all relation RT indexes on 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 | 22 ++++++++ src/test/regress/sql/merge.sql | 25 +++++++++ 3 files changed, 77 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..ea743e89418 100644 --- a/src/test/regress/expected/merge.out +++ b/src/test/regress/expected/merge.out @@ -23,6 +23,28 @@ SELECT t.ctid is not null as matched, t.*, s.* FROM source s FULL OUTER JOIN tar t | 3 | 30 | | (3 rows) +-- target Vars in action expressions above a right MERGE join +CREATE VIEW merge_target_view AS + SELECT tid, balance FROM target; +CREATE FUNCTION merge_target_view_insert() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + RETURN NEW; +END +$$; +CREATE TRIGGER merge_target_view_insert + INSTEAD OF INSERT ON merge_target_view + FOR EACH ROW EXECUTE FUNCTION merge_target_view_insert(); +INSERT INTO source VALUES (1, 1); +MERGE INTO merge_target_view t +USING source s RIGHT JOIN source w ON s.sid = w.sid +ON w.sid = t.tid AND t.tid = 0 +WHEN MATCHED AND s.sid < t.tid THEN DO NOTHING +WHEN NOT MATCHED AND s.delta IS NOT NULL THEN + INSERT (balance) VALUES (s.delta); +TRUNCATE source; +DROP VIEW merge_target_view; +DROP FUNCTION merge_target_view_insert(); ALTER TABLE target OWNER TO regress_merge_privs; ALTER TABLE source OWNER TO regress_merge_privs; CREATE TABLE target2 (tid integer, balance integer) diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql index 37aef4de68a..3b1a1db8d66 100644 --- a/src/test/regress/sql/merge.sql +++ b/src/test/regress/sql/merge.sql @@ -17,6 +17,31 @@ INSERT INTO target VALUES (2, 20); INSERT INTO target VALUES (3, 30); SELECT t.ctid is not null as matched, t.*, s.* FROM source s FULL OUTER JOIN target t ON s.sid = t.tid ORDER BY t.tid, s.sid; +-- target Vars in action expressions above a right MERGE join +CREATE VIEW merge_target_view AS + SELECT tid, balance FROM target; +CREATE FUNCTION merge_target_view_insert() RETURNS trigger +LANGUAGE plpgsql AS $$ +BEGIN + RETURN NEW; +END +$$; +CREATE TRIGGER merge_target_view_insert + INSTEAD OF INSERT ON merge_target_view + FOR EACH ROW EXECUTE FUNCTION merge_target_view_insert(); + +INSERT INTO source VALUES (1, 1); +MERGE INTO merge_target_view t +USING source s RIGHT JOIN source w ON s.sid = w.sid +ON w.sid = t.tid AND t.tid = 0 +WHEN MATCHED AND s.sid < t.tid THEN DO NOTHING +WHEN NOT MATCHED AND s.delta IS NOT NULL THEN + INSERT (balance) VALUES (s.delta); + +TRUNCATE source; +DROP VIEW merge_target_view; +DROP FUNCTION merge_target_view_insert(); + ALTER TABLE target OWNER TO regress_merge_privs; ALTER TABLE source OWNER TO regress_merge_privs; -- 2.43.0