aboutsummaryrefslogtreecommitdiff
path: root/src/backend/executor/nodeModifyTable.c
diff options
context:
space:
mode:
authorDean Rasheed <dean.a.rasheed@gmail.com>2025-01-16 14:57:35 +0000
committerDean Rasheed <dean.a.rasheed@gmail.com>2025-01-16 14:57:35 +0000
commit80feb727c869cc0b2e12bd1543bafa449be9c8e2 (patch)
tree27fb43ef4b09067e3d725e1b918539d492a8550c /src/backend/executor/nodeModifyTable.c
parent7407b2d48cf37bc8847ae6c47dde2164ef2faa34 (diff)
downloadpostgresql-80feb727c869cc0b2e12bd1543bafa449be9c8e2.tar.gz
postgresql-80feb727c869cc0b2e12bd1543bafa449be9c8e2.zip
Add OLD/NEW support to RETURNING in DML queries.
This allows the RETURNING list of INSERT/UPDATE/DELETE/MERGE queries to explicitly return old and new values by using the special aliases "old" and "new", which are automatically added to the query (if not already defined) while parsing its RETURNING list, allowing things like: RETURNING old.colname, new.colname, ... RETURNING old.*, new.* Additionally, a new syntax is supported, allowing the names "old" and "new" to be changed to user-supplied alias names, e.g.: RETURNING WITH (OLD AS o, NEW AS n) o.colname, n.colname, ... This is useful when the names "old" and "new" are already defined, such as inside trigger functions, allowing backwards compatibility to be maintained -- the interpretation of any existing queries that happen to already refer to relations called "old" or "new", or use those as aliases for other relations, is not changed. For an INSERT, old values will generally be NULL, and for a DELETE, new values will generally be NULL, but that may change for an INSERT with an ON CONFLICT ... DO UPDATE clause, or if a query rewrite rule changes the command type. Therefore, we put no restrictions on the use of old and new in any DML queries. Dean Rasheed, reviewed by Jian He and Jeff Davis. Discussion: https://postgr.es/m/CAEZATCWx0J0-v=Qjc6gXzR=KtsdvAE7Ow=D=mu50AgOe+pvisQ@mail.gmail.com
Diffstat (limited to 'src/backend/executor/nodeModifyTable.c')
-rw-r--r--src/backend/executor/nodeModifyTable.c223
1 files changed, 197 insertions, 26 deletions
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 1af8c9caf6c..bc82e035ba2 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -102,6 +102,13 @@ typedef struct ModifyTableContext
TM_FailureData tmfd;
/*
+ * The tuple deleted when doing a cross-partition UPDATE with a RETURNING
+ * clause that refers to OLD columns (converted to the root's tuple
+ * descriptor).
+ */
+ TupleTableSlot *cpDeletedSlot;
+
+ /*
* The tuple projected by the INSERT's RETURNING clause, when doing a
* cross-partition UPDATE
*/
@@ -243,34 +250,81 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
/*
* ExecProcessReturning --- evaluate a RETURNING list
*
+ * context: context for the ModifyTable operation
* resultRelInfo: current result rel
- * tupleSlot: slot holding tuple actually inserted/updated/deleted
+ * cmdType: operation/merge action performed (INSERT, UPDATE, or DELETE)
+ * oldSlot: slot holding old tuple deleted or updated
+ * newSlot: slot holding new tuple inserted or updated
* planSlot: slot holding tuple returned by top subplan node
*
- * Note: If tupleSlot is NULL, the FDW should have already provided econtext's
- * scan tuple.
+ * Note: If oldSlot and newSlot are NULL, the FDW should have already provided
+ * econtext's scan tuple and its old & new tuples are not needed (FDW direct-
+ * modify is disabled if the RETURNING list refers to any OLD/NEW values).
*
* Returns a slot holding the result tuple
*/
static TupleTableSlot *
-ExecProcessReturning(ResultRelInfo *resultRelInfo,
- TupleTableSlot *tupleSlot,
+ExecProcessReturning(ModifyTableContext *context,
+ ResultRelInfo *resultRelInfo,
+ CmdType cmdType,
+ TupleTableSlot *oldSlot,
+ TupleTableSlot *newSlot,
TupleTableSlot *planSlot)
{
+ EState *estate = context->estate;
ProjectionInfo *projectReturning = resultRelInfo->ri_projectReturning;
ExprContext *econtext = projectReturning->pi_exprContext;
/* Make tuple and any needed join variables available to ExecProject */
- if (tupleSlot)
- econtext->ecxt_scantuple = tupleSlot;
+ switch (cmdType)
+ {
+ case CMD_INSERT:
+ case CMD_UPDATE:
+ /* return new tuple by default */
+ if (newSlot)
+ econtext->ecxt_scantuple = newSlot;
+ break;
+
+ case CMD_DELETE:
+ /* return old tuple by default */
+ if (oldSlot)
+ econtext->ecxt_scantuple = oldSlot;
+ break;
+
+ default:
+ elog(ERROR, "unrecognized commandType: %d", (int) cmdType);
+ }
econtext->ecxt_outertuple = planSlot;
+ /* Make old/new tuples available to ExecProject, if required */
+ if (oldSlot)
+ econtext->ecxt_oldtuple = oldSlot;
+ else if (projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD)
+ econtext->ecxt_oldtuple = ExecGetAllNullSlot(estate, resultRelInfo);
+ else
+ econtext->ecxt_oldtuple = NULL; /* No references to OLD columns */
+
+ if (newSlot)
+ econtext->ecxt_newtuple = newSlot;
+ else if (projectReturning->pi_state.flags & EEO_FLAG_HAS_NEW)
+ econtext->ecxt_newtuple = ExecGetAllNullSlot(estate, resultRelInfo);
+ else
+ econtext->ecxt_newtuple = NULL; /* No references to NEW columns */
+
/*
- * RETURNING expressions might reference the tableoid column, so
- * reinitialize tts_tableOid before evaluating them.
+ * Tell ExecProject whether or not the OLD/NEW rows actually exist. This
+ * information is required to evaluate ReturningExpr nodes and also in
+ * ExecEvalSysVar() and ExecEvalWholeRowVar().
*/
- econtext->ecxt_scantuple->tts_tableOid =
- RelationGetRelid(resultRelInfo->ri_RelationDesc);
+ if (oldSlot == NULL)
+ projectReturning->pi_state.flags |= EEO_FLAG_OLD_IS_NULL;
+ else
+ projectReturning->pi_state.flags &= ~EEO_FLAG_OLD_IS_NULL;
+
+ if (newSlot == NULL)
+ projectReturning->pi_state.flags |= EEO_FLAG_NEW_IS_NULL;
+ else
+ projectReturning->pi_state.flags &= ~EEO_FLAG_NEW_IS_NULL;
/* Compute the RETURNING expressions */
return ExecProject(projectReturning);
@@ -1204,7 +1258,56 @@ ExecInsert(ModifyTableContext *context,
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
- result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ {
+ TupleTableSlot *oldSlot = NULL;
+
+ /*
+ * If this is part of a cross-partition UPDATE, and the RETURNING list
+ * refers to any OLD columns, ExecDelete() will have saved the tuple
+ * deleted from the original partition, which we must use here to
+ * compute the OLD column values. Otherwise, all OLD column values
+ * will be NULL.
+ */
+ if (context->cpDeletedSlot)
+ {
+ TupleConversionMap *tupconv_map;
+
+ /*
+ * Convert the OLD tuple to the new partition's format/slot, if
+ * needed. Note that ExceDelete() already converted it to the
+ * root's partition's format/slot.
+ */
+ oldSlot = context->cpDeletedSlot;
+ tupconv_map = ExecGetRootToChildMap(resultRelInfo, estate);
+ if (tupconv_map != NULL)
+ {
+ oldSlot = execute_attr_map_slot(tupconv_map->attrMap,
+ oldSlot,
+ ExecGetReturningSlot(estate,
+ resultRelInfo));
+
+ oldSlot->tts_tableOid = context->cpDeletedSlot->tts_tableOid;
+ ItemPointerCopy(&context->cpDeletedSlot->tts_tid, &oldSlot->tts_tid);
+ }
+ }
+
+ result = ExecProcessReturning(context, resultRelInfo, CMD_INSERT,
+ oldSlot, slot, planSlot);
+
+ /*
+ * For a cross-partition UPDATE, release the old tuple, first making
+ * sure that the result slot has a local copy of any pass-by-reference
+ * values.
+ */
+ if (context->cpDeletedSlot)
+ {
+ ExecMaterializeSlot(result);
+ ExecClearTuple(oldSlot);
+ if (context->cpDeletedSlot != oldSlot)
+ ExecClearTuple(context->cpDeletedSlot);
+ context->cpDeletedSlot = NULL;
+ }
+ }
if (inserted_tuple)
*inserted_tuple = slot;
@@ -1442,6 +1545,7 @@ ExecDelete(ModifyTableContext *context,
Relation resultRelationDesc = resultRelInfo->ri_RelationDesc;
TupleTableSlot *slot = NULL;
TM_Result result;
+ bool saveOld;
if (tupleDeleted)
*tupleDeleted = false;
@@ -1676,8 +1780,17 @@ ldelete:
ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple, changingPart);
- /* Process RETURNING if present and if requested */
- if (processReturning && resultRelInfo->ri_projectReturning)
+ /*
+ * Process RETURNING if present and if requested.
+ *
+ * If this is part of a cross-partition UPDATE, and the RETURNING list
+ * refers to any OLD column values, save the old tuple here for later
+ * processing of the RETURNING list by ExecInsert().
+ */
+ saveOld = changingPart && resultRelInfo->ri_projectReturning &&
+ resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD;
+
+ if (resultRelInfo->ri_projectReturning && (processReturning || saveOld))
{
/*
* We have to put the target tuple into a slot, which means first we
@@ -1705,7 +1818,41 @@ ldelete:
}
}
- rslot = ExecProcessReturning(resultRelInfo, slot, context->planSlot);
+ /*
+ * If required, save the old tuple for later processing of the
+ * RETURNING list by ExecInsert().
+ */
+ if (saveOld)
+ {
+ TupleConversionMap *tupconv_map;
+
+ /*
+ * Convert the tuple into the root partition's format/slot, if
+ * needed. ExecInsert() will then convert it to the new
+ * partition's format/slot, if necessary.
+ */
+ tupconv_map = ExecGetChildToRootMap(resultRelInfo);
+ if (tupconv_map != NULL)
+ {
+ ResultRelInfo *rootRelInfo = context->mtstate->rootResultRelInfo;
+ TupleTableSlot *oldSlot = slot;
+
+ slot = execute_attr_map_slot(tupconv_map->attrMap,
+ slot,
+ ExecGetReturningSlot(estate,
+ rootRelInfo));
+
+ slot->tts_tableOid = oldSlot->tts_tableOid;
+ ItemPointerCopy(&oldSlot->tts_tid, &slot->tts_tid);
+ }
+
+ context->cpDeletedSlot = slot;
+
+ return NULL;
+ }
+
+ rslot = ExecProcessReturning(context, resultRelInfo, CMD_DELETE,
+ slot, NULL, context->planSlot);
/*
* Before releasing the target tuple again, make sure rslot has a
@@ -1758,6 +1905,7 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
bool tuple_deleted;
TupleTableSlot *epqslot = NULL;
+ context->cpDeletedSlot = NULL;
context->cpUpdateReturningSlot = NULL;
*retry_slot = NULL;
@@ -2258,6 +2406,7 @@ ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
* the planSlot. oldtuple is passed to foreign table triggers; it is
* NULL when the foreign table has no relevant triggers.
*
+ * oldSlot contains the old tuple value.
* 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),
@@ -2270,8 +2419,8 @@ ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
*/
static TupleTableSlot *
ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
- ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot,
- bool canSetTag)
+ ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *oldSlot,
+ TupleTableSlot *slot, bool canSetTag)
{
EState *estate = context->estate;
Relation resultRelationDesc = resultRelInfo->ri_RelationDesc;
@@ -2389,7 +2538,6 @@ redo_act:
{
TupleTableSlot *inputslot;
TupleTableSlot *epqslot;
- TupleTableSlot *oldSlot;
if (IsolationUsesXactSnapshot())
ereport(ERROR,
@@ -2504,7 +2652,8 @@ redo_act:
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
- return ExecProcessReturning(resultRelInfo, slot, context->planSlot);
+ return ExecProcessReturning(context, resultRelInfo, CMD_UPDATE,
+ oldSlot, slot, context->planSlot);
return NULL;
}
@@ -2724,16 +2873,23 @@ ExecOnConflictUpdate(ModifyTableContext *context,
/* Execute UPDATE with projection */
*returning = ExecUpdate(context, resultRelInfo,
- conflictTid, NULL,
+ conflictTid, NULL, existing,
resultRelInfo->ri_onConflict->oc_ProjSlot,
canSetTag);
/*
* Clear out existing tuple, as there might not be another conflict among
* the next input rows. Don't want to hold resources till the end of the
- * query.
+ * query. First though, make sure that the returning slot, if any, has a
+ * local copy of any OLD pass-by-reference values, if it refers to any OLD
+ * columns.
*/
+ if (*returning != NULL &&
+ resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD)
+ ExecMaterializeSlot(*returning);
+
ExecClearTuple(existing);
+
return true;
}
@@ -3338,13 +3494,20 @@ lmerge_matched:
switch (commandType)
{
case CMD_UPDATE:
- rslot = ExecProcessReturning(resultRelInfo, newslot,
+ rslot = ExecProcessReturning(context,
+ resultRelInfo,
+ CMD_UPDATE,
+ resultRelInfo->ri_oldTupleSlot,
+ newslot,
context->planSlot);
break;
case CMD_DELETE:
- rslot = ExecProcessReturning(resultRelInfo,
+ rslot = ExecProcessReturning(context,
+ resultRelInfo,
+ CMD_DELETE,
resultRelInfo->ri_oldTupleSlot,
+ NULL,
context->planSlot);
break;
@@ -3894,6 +4057,7 @@ ExecModifyTable(PlanState *pstate)
if (node->mt_merge_pending_not_matched != NULL)
{
context.planSlot = node->mt_merge_pending_not_matched;
+ context.cpDeletedSlot = NULL;
slot = ExecMergeNotMatched(&context, node->resultRelInfo,
node->canSetTag);
@@ -3913,6 +4077,7 @@ ExecModifyTable(PlanState *pstate)
/* Fetch the next row from subplan */
context.planSlot = ExecProcNode(subplanstate);
+ context.cpDeletedSlot = NULL;
/* No more tuples to process? */
if (TupIsNull(context.planSlot))
@@ -3980,9 +4145,15 @@ ExecModifyTable(PlanState *pstate)
* A scan slot containing the data that was actually inserted,
* updated or deleted has already been made available to
* ExecProcessReturning by IterateDirectModify, so no need to
- * provide it here.
+ * provide it here. The individual old and new slots are not
+ * needed, since direct-modify is disabled if the RETURNING list
+ * refers to OLD/NEW values.
*/
- slot = ExecProcessReturning(resultRelInfo, NULL, context.planSlot);
+ Assert((resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD) == 0 &&
+ (resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_NEW) == 0);
+
+ slot = ExecProcessReturning(&context, resultRelInfo, operation,
+ NULL, NULL, context.planSlot);
return slot;
}
@@ -4172,7 +4343,7 @@ ExecModifyTable(PlanState *pstate)
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
- slot, node->canSetTag);
+ oldSlot, slot, node->canSetTag);
if (tuplock)
UnlockTuple(resultRelInfo->ri_RelationDesc, tupleid,
InplaceUpdateTupleLock);