Fix wrong varnullingrels error for MERGE WHEN NOT MATCHED BY SOURCE.
authorDean Rasheed <[email protected]>
Thu, 3 Oct 2024 12:48:32 +0000 (13:48 +0100)
committerDean Rasheed <[email protected]>
Thu, 3 Oct 2024 12:48:32 +0000 (13:48 +0100)
If a MERGE command contains WHEN NOT MATCHED BY SOURCE actions, the
source relation appears on the outer side of the join. Thus, any Vars
referring to the source in the merge join condition, actions, and
RETURNING list should be marked as nullable by the join, since they
are used in the ModifyTable node above the join. Note that this only
applies to the copy of join condition used in the executor to
distinguish MATCHED from NOT MATCHED BY SOURCE cases. Vars in the
original join condition, inside the join node itself, should not be
marked.

Failure to correctly mark these Vars led to a "wrong varnullingrels"
error in the final stage of query planning, in some circumstances. We
happened to get away without this in all previous tests, since they
all involved a ModifyTable node directly on top of the join node, so
that the top plan targetlist coincided with the output of the join,
and the varnullingrels check was more lax. However, if another plan
node, such as a one-time filter Result node, gets inserted between the
ModifyTable node and the join node, then a stricter check is applied,
which fails.

Per bug #18634 from Alexander Lakhin. Thanks to Tom Lane and Richard
Guo for review and analysis.

Back-patch to v17, where WHEN NOT MATCHED BY SOURCE support was added
to MERGE.

Discussion: https://postgr.es/m/18634-db5299c937877f2b%40postgresql.org

src/backend/optimizer/prep/prepjointree.c
src/test/regress/expected/merge.out
src/test/regress/sql/merge.sql

index ca15994fdc732974a8db89c938e4b54db6a57009..4d7f972caf12bf19ee73addca344452d12c1840a 100644 (file)
@@ -282,6 +282,45 @@ transform_MERGE_to_join(Query *parse)
                               bms_make_singleton(parse->mergeTargetRelation),
                               bms_make_singleton(joinrti));
 
+   /*
+    * 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.
+    *
+    * 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
+    * join (in the ModifyTable node) to distinguish between the MATCHED and
+    * NOT MATCHED BY SOURCE cases -- see ExecMergeMatched().  Note that this
+    * creates a modified copy of the join condition, for use above the join,
+    * without modifying the the original join condition, inside the join.
+    */
+   if (jointype == JOIN_LEFT || jointype == JOIN_FULL)
+   {
+       parse->mergeJoinCondition =
+           add_nulling_relids(parse->mergeJoinCondition,
+                              bms_make_singleton(sourcerti),
+                              bms_make_singleton(joinrti));
+
+       foreach_node(MergeAction, action, parse->mergeActionList)
+       {
+           action->qual =
+               add_nulling_relids(action->qual,
+                                  bms_make_singleton(sourcerti),
+                                  bms_make_singleton(joinrti));
+
+           action->targetList = (List *)
+               add_nulling_relids((Node *) action->targetList,
+                                  bms_make_singleton(sourcerti),
+                                  bms_make_singleton(joinrti));
+       }
+
+       parse->returningList = (List *)
+           add_nulling_relids((Node *) parse->returningList,
+                              bms_make_singleton(sourcerti),
+                              bms_make_singleton(joinrti));
+   }
+
    /*
     * If there are any WHEN NOT MATCHED BY SOURCE actions, the executor will
     * use the join condition to distinguish between MATCHED and NOT MATCHED
index 0e59bae1a7fe7a564c71b5eb8974e36cdd74247c..c236f155c0818081ef2ce72e53979a2831ca1742 100644 (file)
@@ -2711,6 +2711,35 @@ SELECT * FROM tgt;
 (0 rows)
 
 DROP TABLE src, tgt;
+--
+-- test for bug #18634 (wrong varnullingrels error)
+--
+CREATE TABLE bug18634t (a int, b int, c text);
+INSERT INTO bug18634t VALUES(1, 10, 'tgt1'), (2, 20, 'tgt2');
+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');
+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
+  RETURNING merge_action(), s.c, t.*;
+ merge_action |  c   | a | b  |  c   
+--------------+------+---+----+------
+ UPDATE       | src1 | 1 |  2 | tgt1
+ DELETE       |      | 2 | 20 | tgt2
+(2 rows)
+
+SELECT * FROM bug18634t;
+ a | b |  c   
+---+---+------
+ 1 | 2 | tgt1
+(1 row)
+
+DROP TABLE bug18634t CASCADE;
+NOTICE:  drop cascades to view bug18634v
+DROP TABLE bug18634s;
 -- prepare
 RESET SESSION AUTHORIZATION;
 -- try a system catalog
index 2a7753c65b08564120047c627991774458c1fc63..ce9981d70b1b86f9f2dfc69abf4601d86acd0e1f 100644 (file)
@@ -1727,6 +1727,27 @@ MERGE INTO tgt USING src ON tgt.a IS NOT DISTINCT FROM src.a
 SELECT * FROM tgt;
 
 DROP TABLE src, tgt;
+--
+-- test for bug #18634 (wrong varnullingrels error)
+--
+CREATE TABLE bug18634t (a int, b int, c text);
+INSERT INTO bug18634t VALUES(1, 10, 'tgt1'), (2, 20, 'tgt2');
+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');
+
+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
+  RETURNING merge_action(), s.c, t.*;
+
+SELECT * FROM bug18634t;
+
+DROP TABLE bug18634t CASCADE;
+DROP TABLE bug18634s;
 
 -- prepare