Expand comments and add an assertion in nodeModifyTable.c.
authorNoah Misch <noah@leadboat.com>
Fri, 28 Jun 2024 02:21:05 +0000 (19:21 -0700)
committerNoah Misch <noah@leadboat.com>
Fri, 28 Jun 2024 02:21:05 +0000 (19:21 -0700)
Most comments concern RELKIND_VIEW.  One addresses the ExecUpdate()
"tupleid" parameter.  A later commit will rely on these facts, but they
hold already.  Back-patch to v12 (all supported versions), the plan for
that commit.

Reviewed (in an earlier version) by Robert Haas.

Discussion: https://postgr.es/m/20240512232923.aa.nmisch@google.com

src/backend/executor/nodeModifyTable.c

index cee60d3659b7e8c4fddd9af37252dc1fdc633b06..a2442b7b0de023ac38f6de60765ce84659ed9919 100644 (file)
  *             values plus row-locating info for UPDATE and MERGE cases, or just the
  *             row-locating info for DELETE cases.
  *
+ *             The relation to modify can be an ordinary table, a view having an
+ *             INSTEAD OF trigger, or a foreign table.  Earlier processing already
+ *             pointed ModifyTable to the underlying relations of any automatically
+ *             updatable view not using an INSTEAD OF trigger, so code here can
+ *             assume it won't have one as a modification target.  This node does
+ *             process ri_WithCheckOptions, which may have expressions from those
+ *             automatically updatable views.
+ *
  *             MERGE runs a join between the source relation and the target table.
  *             If any WHEN NOT MATCHED [BY TARGET] clauses are present, then the join
  *             is an outer join that might output tuples without a matching target
@@ -1398,18 +1406,18 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
  *             DELETE is like UPDATE, except that we delete the tuple and no
  *             index modifications are needed.
  *
- *             When deleting from a table, tupleid identifies the tuple to
- *             delete and oldtuple is NULL.  When deleting from a view,
- *             oldtuple is passed to the INSTEAD OF triggers and identifies
- *             what to delete, and tupleid is invalid.  When deleting from a
- *             foreign table, tupleid is invalid; the FDW has to figure out
- *             which row to delete using data from the planSlot.  oldtuple is
- *             passed to foreign table triggers; it is NULL when the foreign
- *             table has no relevant triggers.  We use tupleDeleted to indicate
- *             whether the tuple is actually deleted, callers can use it to
- *             decide whether to continue the operation.  When this DELETE is a
- *             part of an UPDATE of partition-key, then the slot returned by
- *             EvalPlanQual() is passed back using output parameter epqreturnslot.
+ *             When deleting from a table, tupleid identifies the tuple to delete and
+ *             oldtuple is NULL.  When deleting through a view INSTEAD OF trigger,
+ *             oldtuple is passed to the triggers and identifies what to delete, and
+ *             tupleid is invalid.  When deleting from a foreign table, tupleid is
+ *             invalid; the FDW has to figure out which row to delete using data from
+ *             the planSlot.  oldtuple is passed to foreign table triggers; it is
+ *             NULL when the foreign table has no relevant triggers.  We use
+ *             tupleDeleted to indicate whether the tuple is actually deleted,
+ *             callers can use it to decide whether to continue the operation.  When
+ *             this DELETE is a part of an UPDATE of partition-key, then the slot
+ *             returned by EvalPlanQual() is passed back using output parameter
+ *             epqreturnslot.
  *
  *             Returns RETURNING result if any, otherwise NULL.
  * ----------------------------------------------------------------
@@ -2238,21 +2246,22 @@ ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
  *             is, we don't want to get stuck in an infinite loop
  *             which corrupts your database..
  *
- *             When updating a table, tupleid identifies the tuple to
- *             update and oldtuple is NULL.  When updating a view, oldtuple
- *             is passed to the INSTEAD OF triggers and identifies what to
- *             update, and tupleid is invalid.  When updating a foreign table,
- *             tupleid is invalid; the FDW has to figure out which row to
- *             update using data from the planSlot.  oldtuple is passed to
- *             foreign table triggers; it is NULL when the foreign table has
- *             no relevant triggers.
+ *             When updating a table, tupleid identifies the tuple to update and
+ *             oldtuple is NULL.  When updating through a view INSTEAD OF trigger,
+ *             oldtuple is passed to the triggers and identifies what to update, and
+ *             tupleid is invalid.  When updating a foreign table, tupleid is
+ *             invalid; the FDW has to figure out which row to update using data from
+ *             the planSlot.  oldtuple is passed to foreign table triggers; it is
+ *             NULL when the foreign table has no relevant triggers.
  *
  *             slot contains the new tuple value to be stored.
  *             planSlot is the output of the ModifyTable's subplan; we use it
  *             to access values from other input tables (for RETURNING),
  *             row-ID junk columns, etc.
  *
- *             Returns RETURNING result if any, otherwise NULL.
+ *             Returns RETURNING result if any, otherwise NULL.  On exit, if tupleid
+ *             had identified the tuple to update, it will identify the tuple
+ *             actually updated after EvalPlanQual.
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
@@ -2717,10 +2726,10 @@ ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 
        /*-----
         * If we are dealing with a WHEN MATCHED case, tupleid or oldtuple is
-        * valid, depending on whether the result relation is a table or a view.
-        * We execute the first action for which the additional WHEN MATCHED AND
-        * quals pass.  If an action without quals is found, that action is
-        * executed.
+        * valid, depending on whether the result relation is a table or a view
+        * having an INSTEAD OF trigger.  We execute the first action for which
+        * the additional WHEN MATCHED AND quals pass.  If an action without quals
+        * is found, that action is executed.
         *
         * Similarly, in the WHEN NOT MATCHED BY SOURCE case, tupleid or oldtuple
         * is valid, and we look at the given WHEN NOT MATCHED BY SOURCE actions
@@ -2811,8 +2820,8 @@ ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
  * Check and execute the first qualifying MATCHED or NOT MATCHED BY SOURCE
  * action, depending on whether the join quals are satisfied.  If the target
  * relation is a table, the current target tuple is identified by tupleid.
- * Otherwise, if the target relation is a view, oldtuple is the current target
- * tuple from the view.
+ * Otherwise, if the target relation is a view having an INSTEAD OF trigger,
+ * oldtuple is the current target tuple from the view.
  *
  * We start from the first WHEN MATCHED or WHEN NOT MATCHED BY SOURCE action
  * and check if the WHEN quals pass, if any. If the WHEN quals for the first
@@ -2878,8 +2887,11 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
         */
        Assert(tupleid != NULL || oldtuple != NULL);
        if (oldtuple != NULL)
+       {
+               Assert(resultRelInfo->ri_TrigDesc);
                ExecForceStoreHeapTuple(oldtuple, resultRelInfo->ri_oldTupleSlot,
                                                                false);
+       }
        else if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
                                                                                        tupleid,
                                                                                        SnapshotAny,
@@ -3992,8 +4004,8 @@ ExecModifyTable(PlanState *pstate)
                         * know enough here to set t_tableOid.  Quite separately from
                         * this, the FDW may fetch its own junk attrs to identify the row.
                         *
-                        * Other relevant relkinds, currently limited to views, always
-                        * have a wholerow attribute.
+                        * Other relevant relkinds, currently limited to views having
+                        * INSTEAD OF triggers, always have a wholerow attribute.
                         */
                        else if (AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
                        {