From 5aa827d36138b46c4392ab4b8efe26550cd7e217 Mon Sep 17 00:00:00 2001
From: shihao zhong <zhong950419@gmail.com>
Date: Mon, 5 Oct 2026 11:45:58 -0400
Subject: [PATCH v1 2/5] Mark all source rels nullable for MERGE NOT MATCHED BY
 SOURCE

When the source of MERGE is a join, its Vars reference the rels under
the join, not the join RTE, so they were not marked as nullable by the
MERGE join.  RETURNING could then treat a NOT NULL source column as
never NULL.
---
 src/backend/optimizer/prep/prepjointree.c | 11 +++++++----
 src/test/regress/expected/merge.out       | 13 +++++++++++++
 src/test/regress/sql/merge.sql            |  8 ++++++++
 3 files changed, 28 insertions(+), 4 deletions(-)

diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index dfe320beccd..f47ca4f5615 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -348,27 +348,30 @@ transform_MERGE_to_join(Query *parse)
 	 */
 	if (jointype == JOIN_LEFT || jointype == JOIN_FULL)
 	{
+		/* If the source is a join, its Vars mostly reference the base rels */
+		Relids		sourcerelids = get_relids_in_jointree(source, true, true);
+
 		parse->mergeJoinCondition =
 			add_nulling_relids(parse->mergeJoinCondition,
-							   bms_make_singleton(sourcerti),
+							   sourcerelids,
 							   bms_make_singleton(joinrti));
 
 		foreach_node(MergeAction, action, parse->mergeActionList)
 		{
 			action->qual =
 				add_nulling_relids(action->qual,
-								   bms_make_singleton(sourcerti),
+								   sourcerelids,
 								   bms_make_singleton(joinrti));
 
 			action->targetList = (List *)
 				add_nulling_relids((Node *) action->targetList,
-								   bms_make_singleton(sourcerti),
+								   sourcerelids,
 								   bms_make_singleton(joinrti));
 		}
 
 		parse->returningList = (List *)
 			add_nulling_relids((Node *) parse->returningList,
-							   bms_make_singleton(sourcerti),
+							   sourcerelids,
 							   bms_make_singleton(joinrti));
 	}
 
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 9cb1d87066a..8df955f5180 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2810,6 +2810,19 @@ CREATE VIEW bug18634v AS
   SELECT * FROM bug18634t WHERE EXISTS (SELECT 1 FROM bug18634t);
 CREATE TABLE bug18634s (a int, b int, c text);
 INSERT INTO bug18634s VALUES (1, 2, 'src1');
+-- same with a join as the source, whose Vars must be marked as well
+BEGIN;
+MERGE INTO bug18634v t USING (bug18634s s JOIN bug18634s s2 ON s.a = s2.a) ON s.a = t.a
+  WHEN MATCHED THEN UPDATE SET b = s.b
+  WHEN NOT MATCHED BY SOURCE THEN DELETE
+  RETURNING merge_action(), s.c, s.tableoid IS NULL AS no_source, t.*;
+ merge_action |  c   | no_source | a | b  |  c   
+--------------+------+-----------+---+----+------
+ UPDATE       | src1 | f         | 1 |  2 | tgt1
+ DELETE       |      | t         | 2 | 20 | tgt2
+(2 rows)
+
+ROLLBACK;
 MERGE INTO bug18634v t USING bug18634s s ON s.a = t.a
   WHEN MATCHED THEN UPDATE SET b = s.b
   WHEN NOT MATCHED BY SOURCE THEN DELETE
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
index 2660b19f238..9e136f6cb80 100644
--- a/src/test/regress/sql/merge.sql
+++ b/src/test/regress/sql/merge.sql
@@ -1803,6 +1803,14 @@ CREATE VIEW bug18634v AS
 CREATE TABLE bug18634s (a int, b int, c text);
 INSERT INTO bug18634s VALUES (1, 2, 'src1');
 
+-- same with a join as the source, whose Vars must be marked as well
+BEGIN;
+MERGE INTO bug18634v t USING (bug18634s s JOIN bug18634s s2 ON s.a = s2.a) ON s.a = t.a
+  WHEN MATCHED THEN UPDATE SET b = s.b
+  WHEN NOT MATCHED BY SOURCE THEN DELETE
+  RETURNING merge_action(), s.c, s.tableoid IS NULL AS no_source, t.*;
+ROLLBACK;
+
 MERGE INTO bug18634v t USING bug18634s s ON s.a = t.a
   WHEN MATCHED THEN UPDATE SET b = s.b
   WHEN NOT MATCHED BY SOURCE THEN DELETE
-- 
2.37.1 (Apple Git-137.1)

