diff options
author | Dean Rasheed <dean.a.rasheed@gmail.com> | 2025-01-16 14:57:35 +0000 |
---|---|---|
committer | Dean Rasheed <dean.a.rasheed@gmail.com> | 2025-01-16 14:57:35 +0000 |
commit | 80feb727c869cc0b2e12bd1543bafa449be9c8e2 (patch) | |
tree | 27fb43ef4b09067e3d725e1b918539d492a8550c /src/backend/executor | |
parent | 7407b2d48cf37bc8847ae6c47dde2164ef2faa34 (diff) | |
download | postgresql-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')
-rw-r--r-- | src/backend/executor/execExpr.c | 155 | ||||
-rw-r--r-- | src/backend/executor/execExprInterp.c | 189 | ||||
-rw-r--r-- | src/backend/executor/execMain.c | 1 | ||||
-rw-r--r-- | src/backend/executor/execUtils.c | 28 | ||||
-rw-r--r-- | src/backend/executor/nodeModifyTable.c | 223 |
5 files changed, 554 insertions, 42 deletions
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 7a800df8cab..8f28da4bf94 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -55,10 +55,15 @@ typedef struct ExprSetupInfo { - /* Highest attribute numbers fetched from inner/outer/scan tuple slots: */ + /* + * Highest attribute numbers fetched from inner/outer/scan/old/new tuple + * slots: + */ AttrNumber last_inner; AttrNumber last_outer; AttrNumber last_scan; + AttrNumber last_old; + AttrNumber last_new; /* MULTIEXPR SubPlan nodes appearing in the expression: */ List *multiexpr_subplans; } ExprSetupInfo; @@ -446,8 +451,25 @@ ExecBuildProjectionInfo(List *targetList, /* INDEX_VAR is handled by default case */ default: - /* get the tuple from the relation being scanned */ - scratch.opcode = EEOP_ASSIGN_SCAN_VAR; + + /* + * Get the tuple from the relation being scanned, or the + * old/new tuple slot, if old/new values were requested. + */ + switch (variable->varreturningtype) + { + case VAR_RETURNING_DEFAULT: + scratch.opcode = EEOP_ASSIGN_SCAN_VAR; + break; + case VAR_RETURNING_OLD: + scratch.opcode = EEOP_ASSIGN_OLD_VAR; + state->flags |= EEO_FLAG_HAS_OLD; + break; + case VAR_RETURNING_NEW: + scratch.opcode = EEOP_ASSIGN_NEW_VAR; + state->flags |= EEO_FLAG_HAS_NEW; + break; + } break; } @@ -535,7 +557,7 @@ ExecBuildUpdateProjection(List *targetList, int nAssignableCols; bool sawJunk; Bitmapset *assignedCols; - ExprSetupInfo deform = {0, 0, 0, NIL}; + ExprSetupInfo deform = {0, 0, 0, 0, 0, NIL}; ExprEvalStep scratch = {0}; int outerattnum; ListCell *lc, @@ -924,6 +946,7 @@ ExecInitExprRec(Expr *node, ExprState *state, /* system column */ scratch.d.var.attnum = variable->varattno; scratch.d.var.vartype = variable->vartype; + scratch.d.var.varreturningtype = variable->varreturningtype; switch (variable->varno) { case INNER_VAR: @@ -936,7 +959,20 @@ ExecInitExprRec(Expr *node, ExprState *state, /* INDEX_VAR is handled by default case */ default: - scratch.opcode = EEOP_SCAN_SYSVAR; + switch (variable->varreturningtype) + { + case VAR_RETURNING_DEFAULT: + scratch.opcode = EEOP_SCAN_SYSVAR; + break; + case VAR_RETURNING_OLD: + scratch.opcode = EEOP_OLD_SYSVAR; + state->flags |= EEO_FLAG_HAS_OLD; + break; + case VAR_RETURNING_NEW: + scratch.opcode = EEOP_NEW_SYSVAR; + state->flags |= EEO_FLAG_HAS_NEW; + break; + } break; } } @@ -945,6 +981,7 @@ ExecInitExprRec(Expr *node, ExprState *state, /* regular user column */ scratch.d.var.attnum = variable->varattno - 1; scratch.d.var.vartype = variable->vartype; + scratch.d.var.varreturningtype = variable->varreturningtype; switch (variable->varno) { case INNER_VAR: @@ -957,7 +994,20 @@ ExecInitExprRec(Expr *node, ExprState *state, /* INDEX_VAR is handled by default case */ default: - scratch.opcode = EEOP_SCAN_VAR; + switch (variable->varreturningtype) + { + case VAR_RETURNING_DEFAULT: + scratch.opcode = EEOP_SCAN_VAR; + break; + case VAR_RETURNING_OLD: + scratch.opcode = EEOP_OLD_VAR; + state->flags |= EEO_FLAG_HAS_OLD; + break; + case VAR_RETURNING_NEW: + scratch.opcode = EEOP_NEW_VAR; + state->flags |= EEO_FLAG_HAS_NEW; + break; + } break; } } @@ -2575,6 +2625,34 @@ ExecInitExprRec(Expr *node, ExprState *state, break; } + case T_ReturningExpr: + { + ReturningExpr *rexpr = (ReturningExpr *) node; + int retstep; + + /* Skip expression evaluation if OLD/NEW row doesn't exist */ + scratch.opcode = EEOP_RETURNINGEXPR; + scratch.d.returningexpr.nullflag = rexpr->retold ? + EEO_FLAG_OLD_IS_NULL : EEO_FLAG_NEW_IS_NULL; + scratch.d.returningexpr.jumpdone = -1; /* set below */ + ExprEvalPushStep(state, &scratch); + retstep = state->steps_len - 1; + + /* Steps to evaluate expression to return */ + ExecInitExprRec(rexpr->retexpr, state, resv, resnull); + + /* Jump target used if OLD/NEW row doesn't exist */ + state->steps[retstep].d.returningexpr.jumpdone = state->steps_len; + + /* Update ExprState flags */ + if (rexpr->retold) + state->flags |= EEO_FLAG_HAS_OLD; + else + state->flags |= EEO_FLAG_HAS_NEW; + + break; + } + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); @@ -2786,7 +2864,7 @@ ExecInitSubPlanExpr(SubPlan *subplan, static void ExecCreateExprSetupSteps(ExprState *state, Node *node) { - ExprSetupInfo info = {0, 0, 0, NIL}; + ExprSetupInfo info = {0, 0, 0, 0, 0, NIL}; /* Prescan to find out what we need. */ expr_setup_walker(node, &info); @@ -2809,8 +2887,8 @@ ExecPushExprSetupSteps(ExprState *state, ExprSetupInfo *info) scratch.resnull = NULL; /* - * Add steps deforming the ExprState's inner/outer/scan slots as much as - * required by any Vars appearing in the expression. + * Add steps deforming the ExprState's inner/outer/scan/old/new slots as + * much as required by any Vars appearing in the expression. */ if (info->last_inner > 0) { @@ -2842,6 +2920,26 @@ ExecPushExprSetupSteps(ExprState *state, ExprSetupInfo *info) if (ExecComputeSlotInfo(state, &scratch)) ExprEvalPushStep(state, &scratch); } + if (info->last_old > 0) + { + scratch.opcode = EEOP_OLD_FETCHSOME; + scratch.d.fetch.last_var = info->last_old; + scratch.d.fetch.fixed = false; + scratch.d.fetch.kind = NULL; + scratch.d.fetch.known_desc = NULL; + if (ExecComputeSlotInfo(state, &scratch)) + ExprEvalPushStep(state, &scratch); + } + if (info->last_new > 0) + { + scratch.opcode = EEOP_NEW_FETCHSOME; + scratch.d.fetch.last_var = info->last_new; + scratch.d.fetch.fixed = false; + scratch.d.fetch.kind = NULL; + scratch.d.fetch.known_desc = NULL; + if (ExecComputeSlotInfo(state, &scratch)) + ExprEvalPushStep(state, &scratch); + } /* * Add steps to execute any MULTIEXPR SubPlans appearing in the @@ -2888,7 +2986,18 @@ expr_setup_walker(Node *node, ExprSetupInfo *info) /* INDEX_VAR is handled by default case */ default: - info->last_scan = Max(info->last_scan, attnum); + switch (variable->varreturningtype) + { + case VAR_RETURNING_DEFAULT: + info->last_scan = Max(info->last_scan, attnum); + break; + case VAR_RETURNING_OLD: + info->last_old = Max(info->last_old, attnum); + break; + case VAR_RETURNING_NEW: + info->last_new = Max(info->last_new, attnum); + break; + } break; } return false; @@ -2926,6 +3035,11 @@ expr_setup_walker(Node *node, ExprSetupInfo *info) * evaluation of the expression will have the same type of slot, with an * equivalent descriptor. * + * EEOP_OLD_FETCHSOME and EEOP_NEW_FETCHSOME are used to process RETURNING, if + * OLD/NEW columns are referred to explicitly. In both cases, the tuple + * descriptor comes from the parent scan node, so we treat them the same as + * EEOP_SCAN_FETCHSOME. + * * Returns true if the deforming step is required, false otherwise. */ static bool @@ -2939,7 +3053,9 @@ ExecComputeSlotInfo(ExprState *state, ExprEvalStep *op) Assert(opcode == EEOP_INNER_FETCHSOME || opcode == EEOP_OUTER_FETCHSOME || - opcode == EEOP_SCAN_FETCHSOME); + opcode == EEOP_SCAN_FETCHSOME || + opcode == EEOP_OLD_FETCHSOME || + opcode == EEOP_NEW_FETCHSOME); if (op->d.fetch.known_desc != NULL) { @@ -2991,7 +3107,9 @@ ExecComputeSlotInfo(ExprState *state, ExprEvalStep *op) desc = ExecGetResultType(os); } } - else if (opcode == EEOP_SCAN_FETCHSOME) + else if (opcode == EEOP_SCAN_FETCHSOME || + opcode == EEOP_OLD_FETCHSOME || + opcode == EEOP_NEW_FETCHSOME) { desc = parent->scandesc; @@ -3039,6 +3157,12 @@ ExecInitWholeRowVar(ExprEvalStep *scratch, Var *variable, ExprState *state) scratch->d.wholerow.tupdesc = NULL; /* filled at runtime */ scratch->d.wholerow.junkFilter = NULL; + /* update ExprState flags if Var refers to OLD/NEW */ + if (variable->varreturningtype == VAR_RETURNING_OLD) + state->flags |= EEO_FLAG_HAS_OLD; + else if (variable->varreturningtype == VAR_RETURNING_NEW) + state->flags |= EEO_FLAG_HAS_NEW; + /* * If the input tuple came from a subquery, it might contain "resjunk" * columns (such as GROUP BY or ORDER BY columns), which we don't want to @@ -3541,7 +3665,7 @@ ExecBuildAggTrans(AggState *aggstate, AggStatePerPhase phase, PlanState *parent = &aggstate->ss.ps; ExprEvalStep scratch = {0}; bool isCombine = DO_AGGSPLIT_COMBINE(aggstate->aggsplit); - ExprSetupInfo deform = {0, 0, 0, NIL}; + ExprSetupInfo deform = {0, 0, 0, 0, 0, NIL}; state->expr = (Expr *) aggstate; state->parent = parent; @@ -4082,6 +4206,7 @@ ExecBuildHash32FromAttrs(TupleDesc desc, const TupleTableSlotOps *ops, scratch.resnull = &fcinfo->args[0].isnull; scratch.d.var.attnum = attnum; scratch.d.var.vartype = TupleDescAttr(desc, attnum)->atttypid; + scratch.d.var.varreturningtype = VAR_RETURNING_DEFAULT; ExprEvalPushStep(state, &scratch); @@ -4407,6 +4532,7 @@ ExecBuildGroupingEqual(TupleDesc ldesc, TupleDesc rdesc, scratch.opcode = EEOP_INNER_VAR; scratch.d.var.attnum = attno - 1; scratch.d.var.vartype = latt->atttypid; + scratch.d.var.varreturningtype = VAR_RETURNING_DEFAULT; scratch.resvalue = &fcinfo->args[0].value; scratch.resnull = &fcinfo->args[0].isnull; ExprEvalPushStep(state, &scratch); @@ -4415,6 +4541,7 @@ ExecBuildGroupingEqual(TupleDesc ldesc, TupleDesc rdesc, scratch.opcode = EEOP_OUTER_VAR; scratch.d.var.attnum = attno - 1; scratch.d.var.vartype = ratt->atttypid; + scratch.d.var.varreturningtype = VAR_RETURNING_DEFAULT; scratch.resvalue = &fcinfo->args[1].value; scratch.resnull = &fcinfo->args[1].isnull; ExprEvalPushStep(state, &scratch); @@ -4541,6 +4668,7 @@ ExecBuildParamSetEqual(TupleDesc desc, scratch.opcode = EEOP_INNER_VAR; scratch.d.var.attnum = attno; scratch.d.var.vartype = att->atttypid; + scratch.d.var.varreturningtype = VAR_RETURNING_DEFAULT; scratch.resvalue = &fcinfo->args[0].value; scratch.resnull = &fcinfo->args[0].isnull; ExprEvalPushStep(state, &scratch); @@ -4549,6 +4677,7 @@ ExecBuildParamSetEqual(TupleDesc desc, scratch.opcode = EEOP_OUTER_VAR; scratch.d.var.attnum = attno; scratch.d.var.vartype = att->atttypid; + scratch.d.var.varreturningtype = VAR_RETURNING_DEFAULT; scratch.resvalue = &fcinfo->args[1].value; scratch.resnull = &fcinfo->args[1].isnull; ExprEvalPushStep(state, &scratch); diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 7dfe17b0a86..1127e6f11eb 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -462,6 +462,8 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) TupleTableSlot *innerslot; TupleTableSlot *outerslot; TupleTableSlot *scanslot; + TupleTableSlot *oldslot; + TupleTableSlot *newslot; /* * This array has to be in the same order as enum ExprEvalOp. @@ -472,16 +474,24 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_INNER_FETCHSOME, &&CASE_EEOP_OUTER_FETCHSOME, &&CASE_EEOP_SCAN_FETCHSOME, + &&CASE_EEOP_OLD_FETCHSOME, + &&CASE_EEOP_NEW_FETCHSOME, &&CASE_EEOP_INNER_VAR, &&CASE_EEOP_OUTER_VAR, &&CASE_EEOP_SCAN_VAR, + &&CASE_EEOP_OLD_VAR, + &&CASE_EEOP_NEW_VAR, &&CASE_EEOP_INNER_SYSVAR, &&CASE_EEOP_OUTER_SYSVAR, &&CASE_EEOP_SCAN_SYSVAR, + &&CASE_EEOP_OLD_SYSVAR, + &&CASE_EEOP_NEW_SYSVAR, &&CASE_EEOP_WHOLEROW, &&CASE_EEOP_ASSIGN_INNER_VAR, &&CASE_EEOP_ASSIGN_OUTER_VAR, &&CASE_EEOP_ASSIGN_SCAN_VAR, + &&CASE_EEOP_ASSIGN_OLD_VAR, + &&CASE_EEOP_ASSIGN_NEW_VAR, &&CASE_EEOP_ASSIGN_TMP, &&CASE_EEOP_ASSIGN_TMP_MAKE_RO, &&CASE_EEOP_CONST, @@ -523,6 +533,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_SQLVALUEFUNCTION, &&CASE_EEOP_CURRENTOFEXPR, &&CASE_EEOP_NEXTVALUEEXPR, + &&CASE_EEOP_RETURNINGEXPR, &&CASE_EEOP_ARRAYEXPR, &&CASE_EEOP_ARRAYCOERCE, &&CASE_EEOP_ROW, @@ -591,6 +602,8 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) innerslot = econtext->ecxt_innertuple; outerslot = econtext->ecxt_outertuple; scanslot = econtext->ecxt_scantuple; + oldslot = econtext->ecxt_oldtuple; + newslot = econtext->ecxt_newtuple; #if defined(EEO_USE_COMPUTED_GOTO) EEO_DISPATCH(); @@ -630,6 +643,24 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_OLD_FETCHSOME) + { + CheckOpSlotCompatibility(op, oldslot); + + slot_getsomeattrs(oldslot, op->d.fetch.last_var); + + EEO_NEXT(); + } + + EEO_CASE(EEOP_NEW_FETCHSOME) + { + CheckOpSlotCompatibility(op, newslot); + + slot_getsomeattrs(newslot, op->d.fetch.last_var); + + EEO_NEXT(); + } + EEO_CASE(EEOP_INNER_VAR) { int attnum = op->d.var.attnum; @@ -673,6 +704,32 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_OLD_VAR) + { + int attnum = op->d.var.attnum; + + /* See EEOP_INNER_VAR comments */ + + Assert(attnum >= 0 && attnum < oldslot->tts_nvalid); + *op->resvalue = oldslot->tts_values[attnum]; + *op->resnull = oldslot->tts_isnull[attnum]; + + EEO_NEXT(); + } + + EEO_CASE(EEOP_NEW_VAR) + { + int attnum = op->d.var.attnum; + + /* See EEOP_INNER_VAR comments */ + + Assert(attnum >= 0 && attnum < newslot->tts_nvalid); + *op->resvalue = newslot->tts_values[attnum]; + *op->resnull = newslot->tts_isnull[attnum]; + + EEO_NEXT(); + } + EEO_CASE(EEOP_INNER_SYSVAR) { ExecEvalSysVar(state, op, econtext, innerslot); @@ -691,6 +748,18 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_OLD_SYSVAR) + { + ExecEvalSysVar(state, op, econtext, oldslot); + EEO_NEXT(); + } + + EEO_CASE(EEOP_NEW_SYSVAR) + { + ExecEvalSysVar(state, op, econtext, newslot); + EEO_NEXT(); + } + EEO_CASE(EEOP_WHOLEROW) { /* too complex for an inline implementation */ @@ -750,6 +819,40 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_ASSIGN_OLD_VAR) + { + int resultnum = op->d.assign_var.resultnum; + int attnum = op->d.assign_var.attnum; + + /* + * We do not need CheckVarSlotCompatibility here; that was taken + * care of at compilation time. But see EEOP_INNER_VAR comments. + */ + Assert(attnum >= 0 && attnum < oldslot->tts_nvalid); + Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts); + resultslot->tts_values[resultnum] = oldslot->tts_values[attnum]; + resultslot->tts_isnull[resultnum] = oldslot->tts_isnull[attnum]; + + EEO_NEXT(); + } + + EEO_CASE(EEOP_ASSIGN_NEW_VAR) + { + int resultnum = op->d.assign_var.resultnum; + int attnum = op->d.assign_var.attnum; + + /* + * We do not need CheckVarSlotCompatibility here; that was taken + * care of at compilation time. But see EEOP_INNER_VAR comments. + */ + Assert(attnum >= 0 && attnum < newslot->tts_nvalid); + Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts); + resultslot->tts_values[resultnum] = newslot->tts_values[attnum]; + resultslot->tts_isnull[resultnum] = newslot->tts_isnull[attnum]; + + EEO_NEXT(); + } + EEO_CASE(EEOP_ASSIGN_TMP) { int resultnum = op->d.assign_tmp.resultnum; @@ -1438,6 +1541,23 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_NEXT(); } + EEO_CASE(EEOP_RETURNINGEXPR) + { + /* + * The next op actually evaluates the expression. If the OLD/NEW + * row doesn't exist, skip that and return NULL. + */ + if (state->flags & op->d.returningexpr.nullflag) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + + EEO_JUMP(op->d.returningexpr.jumpdone); + } + + EEO_NEXT(); + } + EEO_CASE(EEOP_ARRAYEXPR) { /* too complex for an inline implementation */ @@ -2119,10 +2239,14 @@ CheckExprStillValid(ExprState *state, ExprContext *econtext) TupleTableSlot *innerslot; TupleTableSlot *outerslot; TupleTableSlot *scanslot; + TupleTableSlot *oldslot; + TupleTableSlot *newslot; innerslot = econtext->ecxt_innertuple; outerslot = econtext->ecxt_outertuple; scanslot = econtext->ecxt_scantuple; + oldslot = econtext->ecxt_oldtuple; + newslot = econtext->ecxt_newtuple; for (int i = 0; i < state->steps_len; i++) { @@ -2153,6 +2277,22 @@ CheckExprStillValid(ExprState *state, ExprContext *econtext) CheckVarSlotCompatibility(scanslot, attnum + 1, op->d.var.vartype); break; } + + case EEOP_OLD_VAR: + { + int attnum = op->d.var.attnum; + + CheckVarSlotCompatibility(oldslot, attnum + 1, op->d.var.vartype); + break; + } + + case EEOP_NEW_VAR: + { + int attnum = op->d.var.attnum; + + CheckVarSlotCompatibility(newslot, attnum + 1, op->d.var.vartype); + break; + } default: break; } @@ -5113,7 +5253,7 @@ void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext) { Var *variable = op->d.wholerow.var; - TupleTableSlot *slot; + TupleTableSlot *slot = NULL; TupleDesc output_tupdesc; MemoryContext oldcontext; HeapTupleHeader dtuple; @@ -5138,8 +5278,40 @@ ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext) /* INDEX_VAR is handled by default case */ default: - /* get the tuple from the relation being scanned */ - slot = econtext->ecxt_scantuple; + + /* + * Get the tuple from the relation being scanned. + * + * By default, this uses the "scan" tuple slot, but a wholerow Var + * in the RETURNING list may explicitly refer to OLD/NEW. If the + * OLD/NEW row doesn't exist, we just return NULL. + */ + switch (variable->varreturningtype) + { + case VAR_RETURNING_DEFAULT: + slot = econtext->ecxt_scantuple; + break; + + case VAR_RETURNING_OLD: + if (state->flags & EEO_FLAG_OLD_IS_NULL) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return; + } + slot = econtext->ecxt_oldtuple; + break; + + case VAR_RETURNING_NEW: + if (state->flags & EEO_FLAG_NEW_IS_NULL) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return; + } + slot = econtext->ecxt_newtuple; + break; + } break; } @@ -5342,6 +5514,17 @@ ExecEvalSysVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext, { Datum d; + /* OLD/NEW system attribute is NULL if OLD/NEW row is NULL */ + if ((op->d.var.varreturningtype == VAR_RETURNING_OLD && + state->flags & EEO_FLAG_OLD_IS_NULL) || + (op->d.var.varreturningtype == VAR_RETURNING_NEW && + state->flags & EEO_FLAG_NEW_IS_NULL)) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return; + } + /* slot_getsysattr has sufficient defenses against bad attnums */ d = slot_getsysattr(slot, op->d.var.attnum, diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 2d28ec65fc4..fb8dba3ab2c 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -1257,6 +1257,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo, resultRelInfo->ri_ReturningSlot = NULL; resultRelInfo->ri_TrigOldSlot = NULL; resultRelInfo->ri_TrigNewSlot = NULL; + resultRelInfo->ri_AllNullSlot = NULL; resultRelInfo->ri_MergeActions[MERGE_WHEN_MATCHED] = NIL; resultRelInfo->ri_MergeActions[MERGE_WHEN_NOT_MATCHED_BY_SOURCE] = NIL; resultRelInfo->ri_MergeActions[MERGE_WHEN_NOT_MATCHED_BY_TARGET] = NIL; diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c index f71899463b8..7c539de5cf2 100644 --- a/src/backend/executor/execUtils.c +++ b/src/backend/executor/execUtils.c @@ -1243,6 +1243,34 @@ ExecGetReturningSlot(EState *estate, ResultRelInfo *relInfo) } /* + * Return a relInfo's all-NULL tuple slot for processing returning tuples. + * + * Note: this slot is intentionally filled with NULLs in every column, and + * should be considered read-only --- the caller must not update it. + */ +TupleTableSlot * +ExecGetAllNullSlot(EState *estate, ResultRelInfo *relInfo) +{ + if (relInfo->ri_AllNullSlot == NULL) + { + Relation rel = relInfo->ri_RelationDesc; + MemoryContext oldcontext = MemoryContextSwitchTo(estate->es_query_cxt); + TupleTableSlot *slot; + + slot = ExecInitExtraTupleSlot(estate, + RelationGetDescr(rel), + table_slot_callbacks(rel)); + ExecStoreAllNullTuple(slot); + + relInfo->ri_AllNullSlot = slot; + + MemoryContextSwitchTo(oldcontext); + } + + return relInfo->ri_AllNullSlot; +} + +/* * Return the map needed to convert given child result relation's tuples to * the rowtype of the query's main target ("root") relation. Note that a * NULL result is valid and means that no conversion is needed. 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); |