I have been playing around with the idea of adding support for OLD/NEW to RETURNING, partly motivated by the discussion on the MERGE RETURNING thread [1], but also because I think it would be a very useful addition for other commands (UPDATE in particular).
This was discussed a long time ago [2], but that previous discussion didn't lead to a workable patch, and so I have taken a different approach here. My first thought was that this would only really make sense for UPDATE and MERGE, since OLD/NEW are pretty pointless for INSERT/DELETE respectively. However... 1. For an INSERT with an ON CONFLICT ... DO UPDATE clause, returning OLD might be very useful, since it provides a way to see which rows conflicted, and return the old conflicting values. 2. If a DELETE is turned into an UPDATE by a rule (e.g., to mark rows as deleted, rather than actually deleting them), then returning NEW can also be useful. (I admit, this is a somewhat obscure use case, but it's still possible.) 3. In a MERGE, we need to be able to handle all 3 command types anyway. 4. It really isn't any extra effort to support INSERT and DELETE. So in the attached very rough patch (no docs, minimal testing) I have just allowed OLD/NEW in RETURNING for all command types (except, I haven't done MERGE here - I think that's best kept as a separate patch). If there is no OLD/NEW row in a particular context, it just returns NULLs. The regression tests contain examples of 1 & 2 above. Based on Robert Haas' suggestion in [2], the patch works by adding a new "varreturningtype" field to Var nodes. This field is set during parse analysis of the returning clause, which adds new namespace aliases for OLD and NEW, if tables with those names/aliases are not already present. So the resulting Var nodes have the same varno/varattno as they would normally have had, but a different varreturningtype. For the most part, the rewriter and parser are then untouched, except for a couple of places necessary to ensure that the new field makes it through correctly. In particular, none of this affects the shape of the final plan produced. All of the work to support the new Var returning type is done in the executor. This turns out to be relatively straightforward, except for cross-partition updates, which was a little trickier since the tuple format of the old row isn't necessarily compatible with the new row, which is in a different partition table and so might have a different column order. One thing that I've explicitly disallowed is returning OLD/NEW for updates to foreign tables. It's possible that could be added in a later patch, but I have no plans to support that right now. One difficult question is what names to use for the new aliases. I think OLD and NEW are the most obvious and natural choices. However, there is a problem - if they are used in a trigger function, they will conflict. In PL/pgSQL, this leads to an error like the following: ERROR: column reference "new.f1" is ambiguous LINE 3: RETURNING new.f1, new.f4 ^ DETAIL: It could refer to either a PL/pgSQL variable or a table column. That's the same error that you'd get if a different alias name had been chosen, and it happened to conflict with a user-defined PL/pgSQL variable, except that in that case, the user could just change their variable name to fix the problem, which is not possible with the automatically-added OLD/NEW trigger variables. As a way round that, I added a way to optionally change the alias used in the RETURNING list, using the following syntax: RETURNING [ WITH ( { OLD | NEW } AS output_alias [, ...] ) ] * | output_expression [ [ AS ] output_name ] [, ...] for example: RETURNING WITH (OLD AS o) o.id, o.val, ... I'm not sure how good a solution that is, but the syntax doesn't look too bad to me (somewhat reminiscent of a WITH-query), and it's only necessary in cases where there is a name conflict. The simpler solution would be to just pick different alias names to start with. The previous thread seemed to settle on BEFORE/AFTER, but I don't find those names particularly intuitive or appealing. Over on [1], PREVIOUS/CURRENT was suggested, which I prefer, but they still don't seem as natural as OLD/NEW. So, as is often the case, naming things turns out to be the hardest problem, which is why I quite like the idea of letting the user pick their own name, if they need to. In most contexts, OLD and NEW will work, so they won't need to. Thoughts? Regards, Dean [1] https://www.postgresql.org/message-id/flat/CAEZATCWePEGQR5LBn-vD6SfeLZafzEm2Qy_L_Oky2=qw2w3...@mail.gmail.com [2] https://www.postgresql.org/message-id/flat/51822C0F.5030807%40gmail.com
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c new file mode 100644 index 2c62b0c..7f6f2c5 --- 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; @@ -440,7 +445,18 @@ ExecBuildProjectionInfo(List *targetList default: /* get the tuple from the relation being scanned */ - scratch.opcode = EEOP_ASSIGN_SCAN_VAR; + switch (variable->varreturningtype) + { + case VAR_RETURNING_OLD: + scratch.opcode = EEOP_ASSIGN_OLD_VAR; + break; + case VAR_RETURNING_NEW: + scratch.opcode = EEOP_ASSIGN_NEW_VAR; + break; + default: + scratch.opcode = EEOP_ASSIGN_SCAN_VAR; + break; + } break; } @@ -528,7 +544,7 @@ ExecBuildUpdateProjection(List *targetLi 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, @@ -929,7 +945,18 @@ ExecInitExprRec(Expr *node, ExprState *s /* INDEX_VAR is handled by default case */ default: - scratch.opcode = EEOP_SCAN_SYSVAR; + switch (variable->varreturningtype) + { + case VAR_RETURNING_OLD: + scratch.opcode = EEOP_OLD_SYSVAR; + break; + case VAR_RETURNING_NEW: + scratch.opcode = EEOP_NEW_SYSVAR; + break; + default: + scratch.opcode = EEOP_SCAN_SYSVAR; + break; + } break; } } @@ -950,7 +977,18 @@ ExecInitExprRec(Expr *node, ExprState *s /* INDEX_VAR is handled by default case */ default: - scratch.opcode = EEOP_SCAN_VAR; + switch (variable->varreturningtype) + { + case VAR_RETURNING_OLD: + scratch.opcode = EEOP_OLD_VAR; + break; + case VAR_RETURNING_NEW: + scratch.opcode = EEOP_NEW_VAR; + break; + default: + scratch.opcode = EEOP_SCAN_VAR; + break; + } break; } } @@ -2683,7 +2721,7 @@ ExecInitFunc(ExprEvalStep *scratch, Expr 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); @@ -2706,8 +2744,8 @@ ExecPushExprSetupSteps(ExprState *state, 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) { @@ -2739,6 +2777,26 @@ ExecPushExprSetupSteps(ExprState *state, 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 @@ -2802,7 +2860,18 @@ expr_setup_walker(Node *node, ExprSetupI /* INDEX_VAR is handled by default case */ default: - info->last_scan = Max(info->last_scan, attnum); + switch (variable->varreturningtype) + { + 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; + default: + info->last_scan = Max(info->last_scan, attnum); + break; + } break; } return false; @@ -2854,7 +2923,9 @@ ExecComputeSlotInfo(ExprState *state, Ex 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) { @@ -2906,7 +2977,9 @@ ExecComputeSlotInfo(ExprState *state, Ex 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; @@ -3455,7 +3528,7 @@ ExecBuildAggTrans(AggState *aggstate, Ag 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; diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c new file mode 100644 index 24c2b60..d06f948 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -157,17 +157,25 @@ static void ExecEvalRowNullInt(ExprState static Datum ExecJustInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustOuterVar(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustScanVar(ExprState *state, ExprContext *econtext, bool *isnull); +static Datum ExecJustOldVar(ExprState *state, ExprContext *econtext, bool *isnull); +static Datum ExecJustNewVar(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustAssignInnerVar(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustAssignOuterVar(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustAssignScanVar(ExprState *state, ExprContext *econtext, bool *isnull); +static Datum ExecJustAssignOldVar(ExprState *state, ExprContext *econtext, bool *isnull); +static Datum ExecJustAssignNewVar(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustApplyFuncToCase(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustConst(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustInnerVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustOuterVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustScanVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); +static Datum ExecJustOldVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); +static Datum ExecJustNewVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustAssignInnerVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustAssignOuterVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); static Datum ExecJustAssignScanVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); +static Datum ExecJustAssignOldVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); +static Datum ExecJustAssignNewVarVirt(ExprState *state, ExprContext *econtext, bool *isnull); /* execution helper functions */ static pg_attribute_always_inline void ExecAggPlainTransByVal(AggState *aggstate, @@ -295,6 +303,18 @@ ExecReadyInterpretedExpr(ExprState *stat state->evalfunc_private = (void *) ExecJustScanVar; return; } + else if (step0 == EEOP_OLD_FETCHSOME && + step1 == EEOP_OLD_VAR) + { + state->evalfunc_private = (void *) ExecJustOldVar; + return; + } + else if (step0 == EEOP_NEW_FETCHSOME && + step1 == EEOP_NEW_VAR) + { + state->evalfunc_private = (void *) ExecJustNewVar; + return; + } else if (step0 == EEOP_INNER_FETCHSOME && step1 == EEOP_ASSIGN_INNER_VAR) { @@ -313,6 +333,18 @@ ExecReadyInterpretedExpr(ExprState *stat state->evalfunc_private = (void *) ExecJustAssignScanVar; return; } + else if (step0 == EEOP_OLD_FETCHSOME && + step1 == EEOP_ASSIGN_OLD_VAR) + { + state->evalfunc_private = (void *) ExecJustAssignOldVar; + return; + } + else if (step0 == EEOP_NEW_FETCHSOME && + step1 == EEOP_ASSIGN_NEW_VAR) + { + state->evalfunc_private = (void *) ExecJustAssignNewVar; + return; + } else if (step0 == EEOP_CASE_TESTVAL && step1 == EEOP_FUNCEXPR_STRICT && state->steps[0].d.casetest.value) @@ -345,6 +377,16 @@ ExecReadyInterpretedExpr(ExprState *stat state->evalfunc_private = (void *) ExecJustScanVarVirt; return; } + else if (step0 == EEOP_OLD_VAR) + { + state->evalfunc_private = (void *) ExecJustOldVarVirt; + return; + } + else if (step0 == EEOP_NEW_VAR) + { + state->evalfunc_private = (void *) ExecJustNewVarVirt; + return; + } else if (step0 == EEOP_ASSIGN_INNER_VAR) { state->evalfunc_private = (void *) ExecJustAssignInnerVarVirt; @@ -360,6 +402,16 @@ ExecReadyInterpretedExpr(ExprState *stat state->evalfunc_private = (void *) ExecJustAssignScanVarVirt; return; } + else if (step0 == EEOP_ASSIGN_OLD_VAR) + { + state->evalfunc_private = (void *) ExecJustAssignOldVarVirt; + return; + } + else if (step0 == EEOP_ASSIGN_NEW_VAR) + { + state->evalfunc_private = (void *) ExecJustAssignNewVarVirt; + return; + } } #if defined(EEO_USE_COMPUTED_GOTO) @@ -399,6 +451,8 @@ ExecInterpExpr(ExprState *state, ExprCon TupleTableSlot *innerslot; TupleTableSlot *outerslot; TupleTableSlot *scanslot; + TupleTableSlot *oldslot; + TupleTableSlot *newslot; /* * This array has to be in the same order as enum ExprEvalOp. @@ -409,16 +463,24 @@ ExecInterpExpr(ExprState *state, ExprCon &&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, @@ -517,6 +579,8 @@ ExecInterpExpr(ExprState *state, ExprCon 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(); @@ -556,6 +620,24 @@ ExecInterpExpr(ExprState *state, ExprCon 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; @@ -599,6 +681,32 @@ ExecInterpExpr(ExprState *state, ExprCon 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); @@ -617,6 +725,18 @@ ExecInterpExpr(ExprState *state, ExprCon 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 */ @@ -676,6 +796,40 @@ ExecInterpExpr(ExprState *state, ExprCon 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; @@ -1880,10 +2034,14 @@ CheckExprStillValid(ExprState *state, Ex 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++) { @@ -1914,6 +2072,22 @@ CheckExprStillValid(ExprState *state, Ex 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; } @@ -2126,6 +2300,20 @@ ExecJustScanVar(ExprState *state, ExprCo return ExecJustVarImpl(state, econtext->ecxt_scantuple, isnull); } +/* Simple reference to OLD Var in RETURNING */ +static Datum +ExecJustOldVar(ExprState *state, ExprContext *econtext, bool *isnull) +{ + return ExecJustVarImpl(state, econtext->ecxt_oldtuple, isnull); +} + +/* Simple reference to NEW Var in RETURNING */ +static Datum +ExecJustNewVar(ExprState *state, ExprContext *econtext, bool *isnull) +{ + return ExecJustVarImpl(state, econtext->ecxt_newtuple, isnull); +} + /* implementation of ExecJustAssign(Inner|Outer|Scan)Var */ static pg_attribute_always_inline Datum ExecJustAssignVarImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull) @@ -2173,6 +2361,20 @@ ExecJustAssignScanVar(ExprState *state, return ExecJustAssignVarImpl(state, econtext->ecxt_scantuple, isnull); } +/* Evaluate OLD Var and assign to appropriate column of result tuple */ +static Datum +ExecJustAssignOldVar(ExprState *state, ExprContext *econtext, bool *isnull) +{ + return ExecJustAssignVarImpl(state, econtext->ecxt_oldtuple, isnull); +} + +/* Evaluate NEW Var and assign to appropriate column of result tuple */ +static Datum +ExecJustAssignNewVar(ExprState *state, ExprContext *econtext, bool *isnull) +{ + return ExecJustAssignVarImpl(state, econtext->ecxt_newtuple, isnull); +} + /* Evaluate CASE_TESTVAL and apply a strict function to it */ static Datum ExecJustApplyFuncToCase(ExprState *state, ExprContext *econtext, bool *isnull) @@ -2264,6 +2466,20 @@ ExecJustScanVarVirt(ExprState *state, Ex return ExecJustVarVirtImpl(state, econtext->ecxt_scantuple, isnull); } +/* Like ExecJustOldVar, optimized for virtual slots */ +static Datum +ExecJustOldVarVirt(ExprState *state, ExprContext *econtext, bool *isnull) +{ + return ExecJustVarVirtImpl(state, econtext->ecxt_oldtuple, isnull); +} + +/* Like ExecJustNewVar, optimized for virtual slots */ +static Datum +ExecJustNewVarVirt(ExprState *state, ExprContext *econtext, bool *isnull) +{ + return ExecJustVarVirtImpl(state, econtext->ecxt_newtuple, isnull); +} + /* implementation of ExecJustAssign(Inner|Outer|Scan)VarVirt */ static pg_attribute_always_inline Datum ExecJustAssignVarVirtImpl(ExprState *state, TupleTableSlot *inslot, bool *isnull) @@ -2307,6 +2523,20 @@ ExecJustAssignScanVarVirt(ExprState *sta return ExecJustAssignVarVirtImpl(state, econtext->ecxt_scantuple, isnull); } +/* Like ExecJustAssignOldVar, optimized for virtual slots */ +static Datum +ExecJustAssignOldVarVirt(ExprState *state, ExprContext *econtext, bool *isnull) +{ + return ExecJustAssignVarVirtImpl(state, econtext->ecxt_oldtuple, isnull); +} + +/* Like ExecJustAssignNewVar, optimized for virtual slots */ +static Datum +ExecJustAssignNewVarVirt(ExprState *state, ExprContext *econtext, bool *isnull) +{ + return ExecJustAssignVarVirtImpl(state, econtext->ecxt_newtuple, isnull); +} + #if defined(EEO_USE_COMPUTED_GOTO) /* * Comparator used when building address->opcode lookup table for @@ -4428,9 +4658,6 @@ ExecEvalSysVar(ExprState *state, ExprEva op->d.var.attnum, op->resnull); *op->resvalue = d; - /* this ought to be unreachable, but it's cheap enough to check */ - if (unlikely(*op->resnull)) - elog(ERROR, "failed to fetch attribute from slot"); } /* diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c new file mode 100644 index b16fbe9..ff44fb2 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -98,6 +98,12 @@ typedef struct ModifyTableContext TM_FailureData tmfd; /* + * The tuple deleted when doing a cross-partition UPDATE with a RETURNING + * clause (converted to the root's tuple descriptor). + */ + TupleTableSlot *cpDeletedSlot; + + /* * The tuple projected by the INSERT's RETURNING clause, when doing a * cross-partition UPDATE */ @@ -238,34 +244,41 @@ ExecCheckPlanOutput(Relation resultRel, * ExecProcessReturning --- evaluate a RETURNING list * * resultRelInfo: current result rel - * tupleSlot: slot holding tuple actually inserted/updated/deleted + * cmdType: operation performed (INSERT, UPDATE, or DELETE only) + * 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/newSlot are NULL, the FDW should have already provided + * econtext's scan/old/new tuples. * * Returns a slot holding the result tuple */ static TupleTableSlot * ExecProcessReturning(ResultRelInfo *resultRelInfo, - TupleTableSlot *tupleSlot, + CmdType cmdType, + TupleTableSlot *oldSlot, + TupleTableSlot *newSlot, TupleTableSlot *planSlot) { 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; + /* Make tuples and any needed join variables available to ExecProject */ + if (oldSlot) + { + econtext->ecxt_oldtuple = oldSlot; + if (cmdType == CMD_DELETE) + econtext->ecxt_scantuple = oldSlot; + } + if (newSlot) + { + econtext->ecxt_newtuple = newSlot; + if (cmdType != CMD_DELETE) + econtext->ecxt_scantuple = newSlot; + } econtext->ecxt_outertuple = planSlot; - /* - * RETURNING expressions might reference the tableoid column, so - * reinitialize tts_tableOid before evaluating them. - */ - econtext->ecxt_scantuple->tts_tableOid = - RelationGetRelid(resultRelInfo->ri_RelationDesc); - /* Compute the RETURNING expressions */ return ExecProject(projectReturning); } @@ -761,6 +774,7 @@ ExecInsert(ModifyTableContext *context, Relation resultRelationDesc; List *recheckIndexes = NIL; TupleTableSlot *planSlot = context->planSlot; + TupleTableSlot *oldSlot; TupleTableSlot *result = NULL; TransitionCaptureState *ar_insert_trig_tcs; ModifyTable *node = (ModifyTable *) mtstate->ps.plan; @@ -1195,7 +1209,60 @@ ExecInsert(ModifyTableContext *context, /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) - result = ExecProcessReturning(resultRelInfo, slot, planSlot); + { + /* + * If this is part of a cross-partition UPDATE, ExecDelete() will have + * saved the tuple deleted from the original partition, which we must + * use here for any OLD columns in the RETURNING list. Otherwise, set + * all OLD columns to 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); + } + } + else + { + oldSlot = ExecGetReturningSlot(estate, resultRelInfo); + + ExecStoreAllNullTuple(oldSlot); + oldSlot->tts_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc); + } + + result = ExecProcessReturning(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; @@ -1664,12 +1731,13 @@ ldelete: ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple, changingPart); /* Process RETURNING if present and if requested */ - if (processReturning && resultRelInfo->ri_projectReturning) + if ((processReturning || changingPart) && resultRelInfo->ri_projectReturning) { /* * We have to put the target tuple into a slot, which means first we * gotta fetch it. We can use the trigger tuple slot. */ + TupleTableSlot *newSlot; TupleTableSlot *rslot; if (resultRelInfo->ri_FdwRoutine) @@ -1692,7 +1760,51 @@ ldelete: } } - rslot = ExecProcessReturning(resultRelInfo, slot, context->planSlot); + /* + * If this is part of a cross-partition UPDATE, save the old tuple for + * later processing of RETURNING in ExecInsert(). + */ + if (changingPart) + { + TupleConversionMap *tupconv_map; + ResultRelInfo *rootRelInfo; + TupleTableSlot *oldSlot; + + /* + * 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) + { + rootRelInfo = context->mtstate->rootResultRelInfo; + 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; + } + + /* + * Use ExecGetTriggerNewSlot() to store the all-NULL new tuple, since + * it is of the right type, and isn't being used for anything else. + */ + newSlot = ExecGetTriggerNewSlot(estate, resultRelInfo); + + ExecStoreAllNullTuple(newSlot); + newSlot->tts_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc); + + rslot = ExecProcessReturning(resultRelInfo, CMD_DELETE, + slot, newSlot, context->planSlot); /* * Before releasing the target tuple again, make sure rslot has a @@ -1744,6 +1856,7 @@ ExecCrossPartitionUpdate(ModifyTableCont bool tuple_deleted; TupleTableSlot *epqslot = NULL; + context->cpDeletedSlot = NULL; context->cpUpdateReturningSlot = NULL; *retry_slot = NULL; @@ -2247,6 +2360,7 @@ ExecCrossPartitionUpdateForeignKey(Modif * 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), @@ -2257,8 +2371,8 @@ ExecCrossPartitionUpdateForeignKey(Modif */ 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; @@ -2373,7 +2487,6 @@ redo_act: { TupleTableSlot *inputslot; TupleTableSlot *epqslot; - TupleTableSlot *oldSlot; if (IsolationUsesXactSnapshot()) ereport(ERROR, @@ -2480,7 +2593,8 @@ redo_act: /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) - return ExecProcessReturning(resultRelInfo, slot, context->planSlot); + return ExecProcessReturning(resultRelInfo, CMD_UPDATE, + oldSlot, slot, context->planSlot); return NULL; } @@ -2692,16 +2806,21 @@ ExecOnConflictUpdate(ModifyTableContext /* 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 (*returning != NULL) + ExecMaterializeSlot(*returning); + ExecClearTuple(existing); + return true; } @@ -3631,6 +3750,7 @@ ExecModifyTable(PlanState *pstate) ResetExprContext(pstate->ps_ExprContext); context.planSlot = ExecProcNode(subplanstate); + context.cpDeletedSlot = NULL; /* No more tuples to process? */ if (TupIsNull(context.planSlot)) @@ -3689,9 +3809,12 @@ 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 RETURNING OLD/NEW is not supported for foreign + * tables. */ - slot = ExecProcessReturning(resultRelInfo, NULL, context.planSlot); + slot = ExecProcessReturning(resultRelInfo, operation, + NULL, NULL, context.planSlot); return slot; } @@ -3838,7 +3961,7 @@ ExecModifyTable(PlanState *pstate) /* Now apply the update. */ slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple, - slot, node->canSetTag); + oldSlot, slot, node->canSetTag); break; case CMD_DELETE: diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c new file mode 100644 index a3a0876..01ca8ee --- a/src/backend/jit/llvm/llvmjit_expr.c +++ b/src/backend/jit/llvm/llvmjit_expr.c @@ -106,6 +106,8 @@ llvm_compile_expr(ExprState *state) LLVMValueRef v_outerslot; LLVMValueRef v_scanslot; LLVMValueRef v_resultslot; + LLVMValueRef v_oldslot; + LLVMValueRef v_newslot; /* nulls/values of slots */ LLVMValueRef v_innervalues; @@ -114,6 +116,10 @@ llvm_compile_expr(ExprState *state) LLVMValueRef v_outernulls; LLVMValueRef v_scanvalues; LLVMValueRef v_scannulls; + LLVMValueRef v_oldvalues; + LLVMValueRef v_oldnulls; + LLVMValueRef v_newvalues; + LLVMValueRef v_newnulls; LLVMValueRef v_resultvalues; LLVMValueRef v_resultnulls; @@ -205,6 +211,16 @@ llvm_compile_expr(ExprState *state) v_state, FIELDNO_EXPRSTATE_RESULTSLOT, "v_resultslot"); + v_oldslot = l_load_struct_gep(b, + StructExprContext, + v_econtext, + FIELDNO_EXPRCONTEXT_OLDTUPLE, + "v_oldslot"); + v_newslot = l_load_struct_gep(b, + StructExprContext, + v_econtext, + FIELDNO_EXPRCONTEXT_NEWTUPLE, + "v_newslot"); /* build global values/isnull pointers */ v_scanvalues = l_load_struct_gep(b, @@ -217,6 +233,26 @@ llvm_compile_expr(ExprState *state) v_scanslot, FIELDNO_TUPLETABLESLOT_ISNULL, "v_scannulls"); + v_oldvalues = l_load_struct_gep(b, + StructTupleTableSlot, + v_oldslot, + FIELDNO_TUPLETABLESLOT_VALUES, + "v_oldvalues"); + v_oldnulls = l_load_struct_gep(b, + StructTupleTableSlot, + v_oldslot, + FIELDNO_TUPLETABLESLOT_ISNULL, + "v_oldnulls"); + v_newvalues = l_load_struct_gep(b, + StructTupleTableSlot, + v_newslot, + FIELDNO_TUPLETABLESLOT_VALUES, + "v_newvalues"); + v_newnulls = l_load_struct_gep(b, + StructTupleTableSlot, + v_newslot, + FIELDNO_TUPLETABLESLOT_ISNULL, + "v_newnulls"); v_innervalues = l_load_struct_gep(b, StructTupleTableSlot, v_innerslot, @@ -302,6 +338,8 @@ llvm_compile_expr(ExprState *state) case EEOP_INNER_FETCHSOME: case EEOP_OUTER_FETCHSOME: case EEOP_SCAN_FETCHSOME: + case EEOP_OLD_FETCHSOME: + case EEOP_NEW_FETCHSOME: { TupleDesc desc = NULL; LLVMValueRef v_slot; @@ -326,8 +364,12 @@ llvm_compile_expr(ExprState *state) v_slot = v_innerslot; else if (opcode == EEOP_OUTER_FETCHSOME) v_slot = v_outerslot; - else + else if (opcode == EEOP_SCAN_FETCHSOME) v_slot = v_scanslot; + else if (opcode == EEOP_OLD_FETCHSOME) + v_slot = v_oldslot; + else + v_slot = v_newslot; /* * Check if all required attributes are available, or @@ -396,6 +438,8 @@ llvm_compile_expr(ExprState *state) case EEOP_INNER_VAR: case EEOP_OUTER_VAR: case EEOP_SCAN_VAR: + case EEOP_OLD_VAR: + case EEOP_NEW_VAR: { LLVMValueRef value, isnull; @@ -413,11 +457,21 @@ llvm_compile_expr(ExprState *state) v_values = v_outervalues; v_nulls = v_outernulls; } - else + else if (opcode == EEOP_SCAN_VAR) { v_values = v_scanvalues; v_nulls = v_scannulls; } + else if (opcode == EEOP_OLD_VAR) + { + v_values = v_oldvalues; + v_nulls = v_oldnulls; + } + else + { + v_values = v_newvalues; + v_nulls = v_newnulls; + } v_attnum = l_int32_const(lc, op->d.var.attnum); value = l_load_gep1(b, TypeSizeT, v_values, v_attnum, ""); @@ -432,6 +486,8 @@ llvm_compile_expr(ExprState *state) case EEOP_INNER_SYSVAR: case EEOP_OUTER_SYSVAR: case EEOP_SCAN_SYSVAR: + case EEOP_OLD_SYSVAR: + case EEOP_NEW_SYSVAR: { LLVMValueRef v_slot; @@ -439,8 +495,12 @@ llvm_compile_expr(ExprState *state) v_slot = v_innerslot; else if (opcode == EEOP_OUTER_SYSVAR) v_slot = v_outerslot; - else + else if (opcode == EEOP_SCAN_SYSVAR) v_slot = v_scanslot; + else if (opcode == EEOP_OLD_SYSVAR) + v_slot = v_oldslot; + else + v_slot = v_newslot; build_EvalXFunc(b, mod, "ExecEvalSysVar", v_state, op, v_econtext, v_slot); @@ -458,6 +518,8 @@ llvm_compile_expr(ExprState *state) 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: { LLVMValueRef v_value; LLVMValueRef v_isnull; @@ -478,11 +540,21 @@ llvm_compile_expr(ExprState *state) v_values = v_outervalues; v_nulls = v_outernulls; } - else + else if (opcode == EEOP_ASSIGN_SCAN_VAR) { v_values = v_scanvalues; v_nulls = v_scannulls; } + else if (opcode == EEOP_ASSIGN_OLD_VAR) + { + v_values = v_oldvalues; + v_nulls = v_oldnulls; + } + else + { + v_values = v_newvalues; + v_nulls = v_newnulls; + } /* load data */ v_attnum = l_int32_const(lc, op->d.assign_var.attnum); diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c new file mode 100644 index c6fb571..20e88a4 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -81,12 +81,14 @@ makeVar(int varno, var->varlevelsup = varlevelsup; /* - * Only a few callers need to make Var nodes with non-null varnullingrels, - * or with varnosyn/varattnosyn different from varno/varattno. We don't - * provide separate arguments for them, but just initialize them to NULL - * and the given varno/varattno. This reduces code clutter and chance of - * error for most callers. + * Only a few callers need to make Var nodes with varreturningtype + * different from VAR_RETURNING_DEFAULT, non-null varnullingrels, or with + * varnosyn/varattnosyn different from varno/varattno. We don't provide + * separate arguments for them, but just initialize them to sensible + * default values. This reduces code clutter and chance of error for most + * callers. */ + var->varreturningtype = VAR_RETURNING_DEFAULT; var->varnullingrels = NULL; var->varnosyn = (Index) varno; var->varattnosyn = varattno; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c new file mode 100644 index c03f4f2..7afb284 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3995,7 +3995,7 @@ raw_expression_tree_walker_impl(Node *no return true; if (WALK(stmt->onConflictClause)) return true; - if (WALK(stmt->returningList)) + if (WALK(stmt->returningClause)) return true; if (WALK(stmt->withClause)) return true; @@ -4011,7 +4011,7 @@ raw_expression_tree_walker_impl(Node *no return true; if (WALK(stmt->whereClause)) return true; - if (WALK(stmt->returningList)) + if (WALK(stmt->returningClause)) return true; if (WALK(stmt->withClause)) return true; @@ -4029,7 +4029,7 @@ raw_expression_tree_walker_impl(Node *no return true; if (WALK(stmt->fromClause)) return true; - if (WALK(stmt->returningList)) + if (WALK(stmt->returningClause)) return true; if (WALK(stmt->withClause)) return true; @@ -4063,6 +4063,16 @@ raw_expression_tree_walker_impl(Node *no return true; } break; + case T_ReturningClause: + { + ReturningClause *returning = (ReturningClause *) node; + + if (WALK(returning->options)) + return true; + if (WALK(returning->exprs)) + return true; + } + break; case T_SelectStmt: { SelectStmt *stmt = (SelectStmt *) node; diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c new file mode 100644 index 34ca6d4..3a3da97 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -7135,6 +7135,34 @@ make_modifytable(PlannerInfo *root, Plan } /* + * Similarly, RETURNING OLD/NEW is not supported for foreign tables. + */ + if (root->parse->returningList && fdwroutine != NULL) + { + List *ret_vars = pull_var_clause((Node *) root->parse->returningList, + PVC_RECURSE_AGGREGATES | + PVC_RECURSE_WINDOWFUNCS | + PVC_INCLUDE_PLACEHOLDERS); + ListCell *lc2; + + foreach(lc2, ret_vars) + { + Var *var = lfirst_node(Var, lc2); + + if (var->varreturningtype != VAR_RETURNING_DEFAULT) + { + RangeTblEntry *rte = planner_rt_fetch(rti, root); + + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot return OLD/NEW values from relation \"%s\"", + get_rel_name(rte->relid)), + errdetail_relkind_not_supported(rte->relkind)); + } + } + } + + /* * Try to modify the foreign table directly if (1) the FDW provides * callback functions needed for that and (2) there are no local * structures that need to be run for each modified row: row-level diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c new file mode 100644 index 73ff407..b14d812 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -2363,7 +2363,8 @@ pullup_replace_vars_callback(Var *var, * expansion with varlevelsup = 0, and then adjust below if needed. */ expandRTE(rcon->target_rte, - var->varno, 0 /* not varlevelsup */ , var->location, + var->varno, 0 /* not varlevelsup */ , + var->varreturningtype, var->location, (var->vartype != RECORDOID), &colnames, &fields); /* Expand the generated per-field Vars, but don't insert PHVs there */ diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c new file mode 100644 index f456b3b..a104052 --- a/src/backend/optimizer/util/appendinfo.c +++ b/src/backend/optimizer/util/appendinfo.c @@ -279,7 +279,10 @@ adjust_appendrel_attrs_mutator(Node *nod elog(ERROR, "attribute %d of relation \"%s\" does not exist", var->varattno, get_rel_name(appinfo->parent_reloid)); if (IsA(newnode, Var)) + { + ((Var *) newnode)->varreturningtype = var->varreturningtype; ((Var *) newnode)->varnullingrels = var->varnullingrels; + } else if (var->varnullingrels != NULL) elog(ERROR, "failed to apply nullingrels to a non-Var"); return newnode; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c new file mode 100644 index 507c101..1f84f70 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -3371,6 +3371,8 @@ eval_const_expressions_mutator(Node *nod fselect->resulttypmod, fselect->resultcollid, ((Var *) arg)->varlevelsup); + /* New Var has same OLD/NEW returning as old one */ + newvar->varreturningtype = ((Var *) arg)->varreturningtype; /* New Var is nullable by same rels as the old one */ newvar->varnullingrels = ((Var *) arg)->varnullingrels; return (Node *) newvar; diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c new file mode 100644 index 7159c77..724f4d4 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -1792,8 +1792,8 @@ build_physical_tlist(PlannerInfo *root, case RTE_NAMEDTUPLESTORE: case RTE_RESULT: /* Not all of these can have dropped cols, but share code anyway */ - expandRTE(rte, varno, 0, -1, true /* include dropped */ , - NULL, &colvars); + expandRTE(rte, varno, 0, VAR_RETURNING_DEFAULT, -1, + true /* include dropped */ , NULL, &colvars); foreach(l, colvars) { var = (Var *) lfirst(l); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c new file mode 100644 index 7a1dfb6..2c0368a --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -74,7 +74,8 @@ static void determineRecursiveColTypes(P Node *larg, List *nrtargetlist); static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt); static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt); -static List *transformReturningList(ParseState *pstate, List *returningList); +static void transformReturningClause(ParseState *pstate, Query *qry, + ReturningClause *returningClause); static Query *transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt); static Query *transformDeclareCursorStmt(ParseState *pstate, @@ -553,7 +554,7 @@ transformDeleteStmt(ParseState *pstate, qual = transformWhereClause(pstate, stmt->whereClause, EXPR_KIND_WHERE, "WHERE"); - qry->returningList = transformReturningList(pstate, stmt->returningList); + transformReturningClause(pstate, qry, stmt->returningClause); /* done building the range table and jointree */ qry->rtable = pstate->p_rtable; @@ -965,7 +966,7 @@ transformInsertStmt(ParseState *pstate, * contain only the target relation, removing any entries added in a * sub-SELECT or VALUES list. */ - if (stmt->onConflictClause || stmt->returningList) + if (stmt->onConflictClause || stmt->returningClause) { pstate->p_namespace = NIL; addNSItemToQuery(pstate, pstate->p_target_nsitem, @@ -978,9 +979,8 @@ transformInsertStmt(ParseState *pstate, stmt->onConflictClause); /* Process RETURNING, if any. */ - if (stmt->returningList) - qry->returningList = transformReturningList(pstate, - stmt->returningList); + if (stmt->returningClause) + transformReturningClause(pstate, qry, stmt->returningClause); /* done building the range table and jointree */ qry->rtable = pstate->p_rtable; @@ -2445,7 +2445,7 @@ transformUpdateStmt(ParseState *pstate, qual = transformWhereClause(pstate, stmt->whereClause, EXPR_KIND_WHERE, "WHERE"); - qry->returningList = transformReturningList(pstate, stmt->returningList); + transformReturningClause(pstate, qry, stmt->returningClause); /* * Now we are done with SELECT-like processing, and can get on with @@ -2538,17 +2538,118 @@ transformUpdateTargetList(ParseState *ps } /* - * transformReturningList - + * buildNSItemForReturning - + * add a ParseNamespaceItem for the OLD or NEW alias in RETURNING. + */ +static void +addNSItemForReturning(ParseState *pstate, const char *aliasname, + VarReturningType returning_type) +{ + List *colnames; + int numattrs; + ParseNamespaceColumn *nscolumns; + ParseNamespaceItem *nsitem; + + /* copy per-column data from the target relation */ + colnames = pstate->p_target_nsitem->p_rte->eref->colnames; + numattrs = list_length(colnames); + + nscolumns = (ParseNamespaceColumn *) + palloc(numattrs * sizeof(ParseNamespaceColumn)); + + memcpy(nscolumns, pstate->p_target_nsitem->p_nscolumns, + numattrs * sizeof(ParseNamespaceColumn)); + + /* mark all columns as returning OLD/NEW */ + for (int i = 0; i < numattrs; i++) + nscolumns[i].p_varreturningtype = returning_type; + + /* build the nsitem, copying most fields from the target relation */ + nsitem = (ParseNamespaceItem *) palloc(sizeof(ParseNamespaceItem)); + nsitem->p_names = makeAlias(aliasname, colnames); + nsitem->p_rte = pstate->p_target_nsitem->p_rte; + nsitem->p_rtindex = pstate->p_target_nsitem->p_rtindex; + nsitem->p_perminfo = pstate->p_target_nsitem->p_perminfo; + nsitem->p_nscolumns = nscolumns; + nsitem->p_lateral_only = pstate->p_target_nsitem->p_lateral_only; + nsitem->p_lateral_ok = pstate->p_target_nsitem->p_lateral_ok; + nsitem->p_returning_type = returning_type; + + /* add it to the query namespace as a table-only item */ + addNSItemToQuery(pstate, nsitem, false, true, false); +} + +/* + * transformReturningClause - * handle a RETURNING clause in INSERT/UPDATE/DELETE */ -static List * -transformReturningList(ParseState *pstate, List *returningList) +static void +transformReturningClause(ParseState *pstate, Query *qry, + ReturningClause *returningClause) { - List *rlist; + ListCell *lc; int save_next_resno; - if (returningList == NIL) - return NIL; /* nothing to do */ + if (returningClause == NULL) + return; /* nothing to do */ + + /* + * Scan RETURNING WITH(...) options for OLD/NEW alias names. Complain if + * there is any conflict with existing relations. + */ + foreach(lc, returningClause->options) + { + ReturningOption *option = lfirst_node(ReturningOption, lc); + + if (refnameNamespaceItem(pstate, NULL, option->name, -1, NULL)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("table name \"%s\" specified more than once", + option->name), + parser_errposition(pstate, option->location)); + + if (option->isNew) + { + if (qry->returningNew != NULL) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("NEW cannot be specified multiple times"), + parser_errposition(pstate, option->location)); + qry->returningNew = option->name; + } + else + { + if (qry->returningOld != NULL) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("OLD cannot be specified multiple times"), + parser_errposition(pstate, option->location)); + qry->returningOld = option->name; + } + } + + /* + * If no OLD/NEW aliases specified, use "old"/"new" unless masked by + * existing relations. + */ + if (qry->returningOld == NULL && + refnameNamespaceItem(pstate, NULL, "old", -1, NULL) == NULL) + qry->returningOld = "old"; + if (qry->returningNew == NULL && + refnameNamespaceItem(pstate, NULL, "new", -1, NULL) == NULL) + qry->returningNew = "new"; + + pstate->p_returning_old = qry->returningOld; + pstate->p_returning_new = qry->returningNew; + + /* + * Add the OLD and NEW aliases to the query namespace, for use in + * expressions in the RETURNING list. + */ + if (qry->returningOld) + addNSItemForReturning(pstate, qry->returningOld, VAR_RETURNING_OLD); + if (qry->returningNew) + addNSItemForReturning(pstate, qry->returningNew, VAR_RETURNING_NEW); /* * We need to assign resnos starting at one in the RETURNING list. Save @@ -2558,8 +2659,10 @@ transformReturningList(ParseState *pstat save_next_resno = pstate->p_next_resno; pstate->p_next_resno = 1; - /* transform RETURNING identically to a SELECT targetlist */ - rlist = transformTargetList(pstate, returningList, EXPR_KIND_RETURNING); + /* transform RETURNING expressions identically to a SELECT targetlist */ + qry->returningList = transformTargetList(pstate, + returningClause->exprs, + EXPR_KIND_RETURNING); /* * Complain if the nonempty tlist expanded to nothing (which is possible @@ -2567,24 +2670,22 @@ transformReturningList(ParseState *pstat * allow this, the parsed Query will look like it didn't have RETURNING, * with results that would probably surprise the user. */ - if (rlist == NIL) + if (qry->returningList == NIL) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("RETURNING must have at least one column"), parser_errposition(pstate, - exprLocation(linitial(returningList))))); + exprLocation(linitial(returningClause->exprs))))); /* mark column origins */ - markTargetListOrigins(pstate, rlist); + markTargetListOrigins(pstate, qry->returningList); /* resolve any still-unresolved output columns as being type text */ if (pstate->p_resolve_unknowns) - resolveTargetListUnknowns(pstate, rlist); + resolveTargetListUnknowns(pstate, qry->returningList); /* restore state */ pstate->p_next_resno = save_next_resno; - - return rlist; } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y new file mode 100644 index d631ac8..28c1383 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -278,6 +278,7 @@ static Node *makeRecursiveViewSelect(cha MergeWhenClause *mergewhen; struct KeyActions *keyactions; struct KeyAction *keyaction; + ReturningClause *retclause; } %type <node> stmt toplevel_stmt schema_stmt routine_body_stmt @@ -445,7 +446,8 @@ static Node *makeRecursiveViewSelect(cha opclass_purpose opt_opfamily transaction_mode_list_or_empty OptTableFuncElementList TableFuncElementList opt_type_modifiers prep_type_clause - execute_param_clause using_clause returning_clause + execute_param_clause using_clause + returning_with_clause returning_options opt_enum_val_list enum_val_list table_func_column_list create_generic_options alter_generic_options relation_expr_list dostmt_opt_list @@ -454,6 +456,9 @@ static Node *makeRecursiveViewSelect(cha vacuum_relation_list opt_vacuum_relation_list drop_option_list pub_obj_list +%type <retclause> returning_clause +%type <node> returning_option +%type <boolean> returning_option_is_new %type <node> opt_routine_body %type <groupclause> group_clause %type <list> group_by_list @@ -12049,7 +12054,7 @@ InsertStmt: { $5->relation = $4; $5->onConflictClause = $6; - $5->returningList = $7; + $5->returningClause = $7; $5->withClause = $1; $$ = (Node *) $5; } @@ -12182,8 +12187,45 @@ opt_conf_expr: ; returning_clause: - RETURNING target_list { $$ = $2; } - | /* EMPTY */ { $$ = NIL; } + RETURNING returning_with_clause target_list + { + ReturningClause *n = makeNode(ReturningClause); + + n->options = $2; + n->exprs = $3; + $$ = n; + } + | /* EMPTY */ + { + $$ = NULL; + } + ; + +returning_with_clause: + WITH '(' returning_options ')' { $$ = $3; } + | /* EMPTY */ { $$ = NIL; } + ; + +returning_options: + returning_option { $$ = list_make1($1); } + | returning_options ',' returning_option { $$ = lappend($1, $3); } + ; + +returning_option: + returning_option_is_new AS ColId + { + ReturningOption *n = makeNode(ReturningOption); + + n->isNew = $1; + n->name = $3; + n->location = @1; + $$ = (Node *) n; + } + ; + +returning_option_is_new: + OLD { $$ = false; } + | NEW { $$ = true; } ; @@ -12202,7 +12244,7 @@ DeleteStmt: opt_with_clause DELETE_P FRO n->relation = $4; n->usingClause = $5; n->whereClause = $6; - n->returningList = $7; + n->returningClause = $7; n->withClause = $1; $$ = (Node *) n; } @@ -12276,7 +12318,7 @@ UpdateStmt: opt_with_clause UPDATE relat n->targetList = $5; n->fromClause = $6; n->whereClause = $7; - n->returningList = $8; + n->returningClause = $8; n->withClause = $1; $$ = (Node *) n; } diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c new file mode 100644 index 334b9b4..4dabb62 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -1581,6 +1581,7 @@ transformFromClauseItem(ParseState *psta jnsitem->p_cols_visible = true; jnsitem->p_lateral_only = false; jnsitem->p_lateral_ok = true; + jnsitem->p_returning_type = VAR_RETURNING_DEFAULT; /* Per SQL, we must check for alias conflicts */ checkNameSpaceConflicts(pstate, list_make1(jnsitem), my_namespace); my_namespace = lappend(my_namespace, jnsitem); @@ -1643,6 +1644,7 @@ buildVarFromNSColumn(ParseState *pstate, nscol->p_varcollid, 0); /* makeVar doesn't offer parameters for these, so set by hand: */ + var->varreturningtype = nscol->p_varreturningtype; var->varnosyn = nscol->p_varnosyn; var->varattnosyn = nscol->p_varattnosyn; diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c new file mode 100644 index 64c582c..14a82d6 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -2588,6 +2588,9 @@ transformWholeRowRef(ParseState *pstate, result = makeWholeRowVar(nsitem->p_rte, nsitem->p_rtindex, sublevels_up, true); + /* mark Var for RETURNING OLD/NEW, as necessary */ + result->varreturningtype = nsitem->p_returning_type; + /* location is not filled in by makeWholeRowVar */ result->location = location; @@ -2610,9 +2613,8 @@ transformWholeRowRef(ParseState *pstate, * are in the RTE. We needn't worry about marking the RTE for SELECT * access, as the common columns are surely so marked already. */ - expandRTE(nsitem->p_rte, nsitem->p_rtindex, - sublevels_up, location, false, - NULL, &fields); + expandRTE(nsitem->p_rte, nsitem->p_rtindex, sublevels_up, + nsitem->p_returning_type, location, false, NULL, &fields); rowexpr = makeNode(RowExpr); rowexpr->args = list_truncate(fields, list_length(nsitem->p_names->colnames)); diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c new file mode 100644 index 864ea9b..92c9cd5 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -91,11 +91,13 @@ static void markRTEForSelectPriv(ParseSt int rtindex, AttrNumber col); static void expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, + VarReturningType returning_type, int location, bool include_dropped, List **colnames, List **colvars); static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, + VarReturningType returning_type, int location, bool include_dropped, List **colnames, List **colvars); static int specialAttNum(const char *attname); @@ -763,6 +765,9 @@ scanNSItemForColumn(ParseState *pstate, } var->location = location; + /* Mark Var for RETURNING OLD/NEW, as necessary */ + var->varreturningtype = nsitem->p_returning_type; + /* Mark Var if it's nulled by any outer joins */ markNullableIfNeeded(pstate, var); @@ -1336,6 +1341,7 @@ buildNSItemFromTupleDesc(RangeTblEntry * nsitem->p_cols_visible = true; nsitem->p_lateral_only = false; nsitem->p_lateral_ok = true; + nsitem->p_returning_type = VAR_RETURNING_DEFAULT; return nsitem; } @@ -1399,6 +1405,7 @@ buildNSItemFromLists(RangeTblEntry *rte, nsitem->p_cols_visible = true; nsitem->p_lateral_only = false; nsitem->p_lateral_ok = true; + nsitem->p_returning_type = VAR_RETURNING_DEFAULT; return nsitem; } @@ -2305,6 +2312,7 @@ addRangeTableEntryForJoin(ParseState *ps nsitem->p_cols_visible = true; nsitem->p_lateral_only = false; nsitem->p_lateral_ok = true; + nsitem->p_returning_type = VAR_RETURNING_DEFAULT; return nsitem; } @@ -2653,9 +2661,10 @@ addNSItemToQuery(ParseState *pstate, Par * results. If include_dropped is true then empty strings and NULL constants * (not Vars!) are returned for dropped columns. * - * rtindex, sublevels_up, and location are the varno, varlevelsup, and location - * values to use in the created Vars. Ordinarily rtindex should match the - * actual position of the RTE in its rangetable. + * rtindex, sublevels_up, returning_type, and location are the varno, + * varlevelsup, varreturningtype, and location values to use in the created + * Vars. Ordinarily rtindex should match the actual position of the RTE in + * its rangetable. * * The output lists go into *colnames and *colvars. * If only one of the two kinds of output list is needed, pass NULL for the @@ -2663,6 +2672,7 @@ addNSItemToQuery(ParseState *pstate, Par */ void expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, + VarReturningType returning_type, int location, bool include_dropped, List **colnames, List **colvars) { @@ -2678,7 +2688,7 @@ expandRTE(RangeTblEntry *rte, int rtinde case RTE_RELATION: /* Ordinary relation RTE */ expandRelation(rte->relid, rte->eref, - rtindex, sublevels_up, location, + rtindex, sublevels_up, returning_type, location, include_dropped, colnames, colvars); break; case RTE_SUBQUERY: @@ -2757,7 +2767,8 @@ expandRTE(RangeTblEntry *rte, int rtinde Assert(tupdesc); expandTupleDesc(tupdesc, rte->eref, rtfunc->funccolcount, atts_done, - rtindex, sublevels_up, location, + rtindex, sublevels_up, + returning_type, location, include_dropped, colnames, colvars); } else if (functypclass == TYPEFUNC_SCALAR) @@ -3016,6 +3027,7 @@ expandRTE(RangeTblEntry *rte, int rtinde */ static void expandRelation(Oid relid, Alias *eref, int rtindex, int sublevels_up, + VarReturningType returning_type, int location, bool include_dropped, List **colnames, List **colvars) { @@ -3024,7 +3036,7 @@ expandRelation(Oid relid, Alias *eref, i /* Get the tupledesc and turn it over to expandTupleDesc */ rel = relation_open(relid, AccessShareLock); expandTupleDesc(rel->rd_att, eref, rel->rd_att->natts, 0, - rtindex, sublevels_up, + rtindex, sublevels_up, returning_type, location, include_dropped, colnames, colvars); relation_close(rel, AccessShareLock); @@ -3042,6 +3054,7 @@ expandRelation(Oid relid, Alias *eref, i static void expandTupleDesc(TupleDesc tupdesc, Alias *eref, int count, int offset, int rtindex, int sublevels_up, + VarReturningType returning_type, int location, bool include_dropped, List **colnames, List **colvars) { @@ -3102,6 +3115,7 @@ expandTupleDesc(TupleDesc tupdesc, Alias attr->atttypid, attr->atttypmod, attr->attcollation, sublevels_up); + varnode->varreturningtype = returning_type; varnode->location = location; *colvars = lappend(*colvars, varnode); @@ -3154,6 +3168,7 @@ expandNSItemVars(ParseState *pstate, Par nscol->p_varcollid, sublevels_up); /* makeVar doesn't offer parameters for these, so set by hand: */ + var->varreturningtype = nscol->p_varreturningtype; var->varnosyn = nscol->p_varnosyn; var->varattnosyn = nscol->p_varattnosyn; var->location = location; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c new file mode 100644 index 3bc62ac..1d1a005 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1534,8 +1534,8 @@ expandRecordVariable(ParseState *pstate, *lvar; int i; - expandRTE(rte, var->varno, 0, var->location, false, - &names, &vars); + expandRTE(rte, var->varno, 0, var->varreturningtype, + var->location, false, &names, &vars); tupleDesc = CreateTemplateTupleDesc(list_length(vars)); i = 1; diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c new file mode 100644 index 41a3623..53c574a --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -663,15 +663,14 @@ rewriteRuleAction(Query *parsetree, errmsg("cannot have RETURNING lists in multiple rules"))); *returning_flag = true; rule_action->returningList = (List *) - ReplaceVarsFromTargetList((Node *) parsetree->returningList, - parsetree->resultRelation, - 0, - rt_fetch(parsetree->resultRelation, - parsetree->rtable), - rule_action->returningList, - REPLACEVARS_REPORT_ERROR, - 0, - &rule_action->hasSubLinks); + ReplaceReturningVarsFromTargetList((Node *) parsetree->returningList, + parsetree->resultRelation, + 0, + rt_fetch(parsetree->resultRelation, + parsetree->rtable), + rule_action->returningList, + rule_action->resultRelation, + &rule_action->hasSubLinks); /* * There could have been some SubLinks in parsetree's returningList, @@ -3321,14 +3320,13 @@ rewriteTargetView(Query *parsetree, Rela * reference the appropriate column of the base relation instead. */ parsetree = (Query *) - ReplaceVarsFromTargetList((Node *) parsetree, - parsetree->resultRelation, - 0, - view_rte, - view_targetlist, - REPLACEVARS_REPORT_ERROR, - 0, - NULL); + ReplaceReturningVarsFromTargetList((Node *) parsetree, + parsetree->resultRelation, + 0, + view_rte, + view_targetlist, + new_rt_index, + NULL); /* * Update all other RTI references in the query that point to the view diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c new file mode 100644 index 32bd2f1..ccc41a5 --- a/src/backend/rewrite/rewriteManip.c +++ b/src/backend/rewrite/rewriteManip.c @@ -875,6 +875,68 @@ IncrementVarSublevelsUp_rtable(List *rta QTW_EXAMINE_RTES_BEFORE); } +/* + * SetVarReturningType - adjust Vars by setting their returning type + * + * Find all Var nodes referring to the specified result relation in the given + * expression and set their varreturningtype to the specified value. + * + * NOTE: although this has the form of a walker, we cheat and modify the + * Var nodes in-place. The given expression tree should have been copied + * earlier to ensure that no unwanted side-effects occur! + */ + +typedef struct +{ + int result_relation; + int sublevels_up; + VarReturningType returning_type; +} SetVarReturningType_context; + +static bool +SetVarReturningType_walker(Node *node, SetVarReturningType_context *context) +{ + if (node == NULL) + return false; + if (IsA(node, Var)) + { + Var *var = (Var *) node; + + if (var->varno == context->result_relation && + var->varlevelsup == context->sublevels_up) + var->varreturningtype = context->returning_type; + + return false; + } + + if (IsA(node, Query)) + { + /* Recurse into subselects */ + bool result; + + context->sublevels_up++; + result = query_tree_walker((Query *) node, SetVarReturningType_walker, + (void *) context, 0); + context->sublevels_up--; + return result; + } + return expression_tree_walker(node, SetVarReturningType_walker, + (void *) context); +} + +static void +SetVarReturningType(Node *node, int result_relation, int sublevels_up, + VarReturningType returning_type) +{ + SetVarReturningType_context context; + + context.result_relation = result_relation; + context.sublevels_up = sublevels_up; + context.returning_type = returning_type; + + /* Expect to start with an expression */ + SetVarReturningType_walker(node, &context); +} /* * rangeTableEntry_used - detect whether an RTE is referenced somewhere @@ -1675,8 +1737,8 @@ ReplaceVarsFromTargetList_callback(Var * * the RowExpr for use of the executor and ruleutils.c. */ expandRTE(rcon->target_rte, - var->varno, var->varlevelsup, var->location, - (var->vartype != RECORDOID), + var->varno, var->varlevelsup, VAR_RETURNING_DEFAULT, + var->location, (var->vartype != RECORDOID), &colnames, &fields); /* Adjust the generated per-field Vars... */ fields = (List *) replace_rte_variables_mutator((Node *) fields, @@ -1778,3 +1840,58 @@ ReplaceVarsFromTargetList(Node *node, (void *) &context, outer_hasSubLinks); } + + +/* + * ReplaceReturningVarsFromTargetList - + * replace RETURNING list Vars with items from a targetlist + * + * This is equivalent to calling ReplaceVarsFromTargetList() with a + * nomatch_option of REPLACEVARS_REPORT_ERROR, but with the added effect of + * copying varreturningtype onto any Vars referring to new_result_relation, + * allowing RETURNING OLD/NEW to work in the rewritten query. + */ + +typedef struct +{ + ReplaceVarsFromTargetList_context rv_con; + int new_result_relation; +} ReplaceReturningVarsFromTargetList_context; + +static Node * +ReplaceReturningVarsFromTargetList_callback(Var *var, + replace_rte_variables_context *context) +{ + ReplaceReturningVarsFromTargetList_context *rcon = (ReplaceReturningVarsFromTargetList_context *) context->callback_arg; + Node *newnode; + + newnode = ReplaceVarsFromTargetList_callback(var, context); + + if (var->varreturningtype != VAR_RETURNING_DEFAULT) + SetVarReturningType((Node *) newnode, rcon->new_result_relation, + var->varlevelsup, var->varreturningtype); + + return newnode; +} + +Node * +ReplaceReturningVarsFromTargetList(Node *node, + int target_varno, int sublevels_up, + RangeTblEntry *target_rte, + List *targetlist, + int new_result_relation, + bool *outer_hasSubLinks) +{ + ReplaceReturningVarsFromTargetList_context context; + + context.rv_con.target_rte = target_rte; + context.rv_con.targetlist = targetlist; + context.rv_con.nomatch_option = REPLACEVARS_REPORT_ERROR; + context.rv_con.nomatch_varno = 0; + context.new_result_relation = new_result_relation; + + return replace_rte_variables(node, target_varno, sublevels_up, + ReplaceReturningVarsFromTargetList_callback, + (void *) &context, + outer_hasSubLinks); +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c new file mode 100644 index ed7f40f..aa5a826 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -117,6 +117,8 @@ typedef struct List *namespaces; /* List of deparse_namespace nodes */ List *windowClause; /* Current query level's WINDOW clause */ List *windowTList; /* targetlist for resolving WINDOW clause */ + char *returningOld; /* alias for OLD in RETURNING list */ + char *returningNew; /* alias for NEW in RETURNING list */ int prettyFlags; /* enabling of pretty-print functions */ int wrapColumn; /* max line length, or -1 for no limit */ int indentLevel; /* current indent level for pretty-print */ @@ -418,6 +420,8 @@ static void get_basic_select_query(Query TupleDesc resultDesc, bool colNamesVisible); static void get_target_list(List *targetList, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); +static void get_returning_clause(Query *query, deparse_context *context, + bool colNamesVisible); static void get_setop_query(Node *setOp, Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible); @@ -1083,6 +1087,8 @@ pg_get_triggerdef_worker(Oid trigid, boo context.namespaces = list_make1(&dpns); context.windowClause = NIL; context.windowTList = NIL; + context.returningOld = NULL; + context.returningNew = NULL; context.varprefix = true; context.prettyFlags = GET_PRETTY_FLAGS(pretty); context.wrapColumn = WRAP_COLUMN_DEFAULT; @@ -3636,6 +3642,8 @@ deparse_expression_pretty(Node *expr, Li context.namespaces = dpcontext; context.windowClause = NIL; context.windowTList = NIL; + context.returningOld = NULL; + context.returningNew = NULL; context.varprefix = forceprefix; context.prettyFlags = prettyFlags; context.wrapColumn = WRAP_COLUMN_DEFAULT; @@ -4367,8 +4375,8 @@ set_relation_column_names(deparse_namesp if (rte->rtekind == RTE_FUNCTION && rte->functions != NIL) { /* Since we're not creating Vars, rtindex etc. don't matter */ - expandRTE(rte, 1, 0, -1, true /* include dropped */ , - &colnames, NULL); + expandRTE(rte, 1, 0, VAR_RETURNING_DEFAULT, -1, + true /* include dropped */ , &colnames, NULL); } else colnames = rte->eref->colnames; @@ -5284,6 +5292,8 @@ make_ruledef(StringInfo buf, HeapTuple r context.namespaces = list_make1(&dpns); context.windowClause = NIL; context.windowTList = NIL; + context.returningOld = NULL; + context.returningNew = NULL; context.varprefix = (list_length(query->rtable) != 1); context.prettyFlags = prettyFlags; context.wrapColumn = WRAP_COLUMN_DEFAULT; @@ -5452,6 +5462,8 @@ get_query_def(Query *query, StringInfo b context.namespaces = lcons(&dpns, list_copy(parentnamespace)); context.windowClause = NIL; context.windowTList = NIL; + context.returningOld = NULL; + context.returningNew = NULL; context.varprefix = (parentnamespace != NIL || list_length(query->rtable) != 1); context.prettyFlags = prettyFlags; @@ -6157,6 +6169,52 @@ get_target_list(List *targetList, depars } static void +get_returning_clause(Query *query, deparse_context *context, + bool colNamesVisible) +{ + StringInfo buf = context->buf; + + if (query->returningList) + { + char *saved_returning_old = context->returningOld; + char *saved_returning_new = context->returningNew; + bool have_with = false; + + appendContextKeyword(context, " RETURNING", + -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); + + /* Add WITH options, if they're not the defaults */ + if (query->returningOld && strcmp(query->returningOld, "old") != 0) + { + appendStringInfo(buf, " WITH (OLD AS %s", query->returningOld); + have_with = true; + } + if (query->returningNew && strcmp(query->returningNew, "new") != 0) + { + if (have_with) + appendStringInfo(buf, ", "); + else + { + appendStringInfo(buf, " WITH ("); + have_with = true; + } + appendStringInfo(buf, "NEW AS %s", query->returningNew); + } + if (have_with) + appendStringInfo(buf, ")"); + + /* Add the returning expressions themselves (may refer to OLD/NEW) */ + context->returningOld = query->returningOld; + context->returningNew = query->returningNew; + + get_target_list(query->returningList, context, NULL, colNamesVisible); + + context->returningOld = saved_returning_old; + context->returningNew = saved_returning_new; + } +} + +static void get_setop_query(Node *setOp, Query *query, deparse_context *context, TupleDesc resultDesc, bool colNamesVisible) { @@ -6810,12 +6868,7 @@ get_insert_query_def(Query *query, depar } /* Add RETURNING if present */ - if (query->returningList) - { - appendContextKeyword(context, " RETURNING", - -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); - get_target_list(query->returningList, context, NULL, colNamesVisible); - } + get_returning_clause(query, context, colNamesVisible); } @@ -6867,12 +6920,7 @@ get_update_query_def(Query *query, depar } /* Add RETURNING if present */ - if (query->returningList) - { - appendContextKeyword(context, " RETURNING", - -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); - get_target_list(query->returningList, context, NULL, colNamesVisible); - } + get_returning_clause(query, context, colNamesVisible); } @@ -7071,12 +7119,7 @@ get_delete_query_def(Query *query, depar } /* Add RETURNING if present */ - if (query->returningList) - { - appendContextKeyword(context, " RETURNING", - -PRETTYINDENT_STD, PRETTYINDENT_STD, 1); - get_target_list(query->returningList, context, NULL, colNamesVisible); - } + get_returning_clause(query, context, colNamesVisible); } @@ -7345,7 +7388,13 @@ get_variable(Var *var, int levelsup, boo } rte = rt_fetch(varno, dpns->rtable); - refname = (char *) list_nth(dpns->rtable_names, varno - 1); + if (var->varreturningtype == VAR_RETURNING_OLD) + refname = context->returningOld; + else if (var->varreturningtype == VAR_RETURNING_NEW) + refname = context->returningNew; + else + refname = (char *) list_nth(dpns->rtable_names, varno - 1); + colinfo = deparse_columns_fetch(varno, dpns); attnum = varattno; } diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h new file mode 100644 index 048573c..f80b563 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -71,16 +71,22 @@ typedef enum ExprEvalOp EEOP_INNER_FETCHSOME, EEOP_OUTER_FETCHSOME, EEOP_SCAN_FETCHSOME, + EEOP_OLD_FETCHSOME, + EEOP_NEW_FETCHSOME, /* compute non-system Var value */ EEOP_INNER_VAR, EEOP_OUTER_VAR, EEOP_SCAN_VAR, + EEOP_OLD_VAR, + EEOP_NEW_VAR, /* compute system Var value */ EEOP_INNER_SYSVAR, EEOP_OUTER_SYSVAR, EEOP_SCAN_SYSVAR, + EEOP_OLD_SYSVAR, + EEOP_NEW_SYSVAR, /* compute wholerow Var */ EEOP_WHOLEROW, @@ -93,6 +99,8 @@ typedef enum ExprEvalOp EEOP_ASSIGN_INNER_VAR, EEOP_ASSIGN_OUTER_VAR, EEOP_ASSIGN_SCAN_VAR, + EEOP_ASSIGN_OLD_VAR, + EEOP_ASSIGN_NEW_VAR, /* assign ExprState's resvalue/resnull to a column of its resultslot */ EEOP_ASSIGN_TMP, diff --git a/src/include/executor/tuptable.h b/src/include/executor/tuptable.h new file mode 100644 index 4210d6d..9017701 --- a/src/include/executor/tuptable.h +++ b/src/include/executor/tuptable.h @@ -410,12 +410,21 @@ slot_getsysattr(TupleTableSlot *slot, in { Assert(attnum < 0); /* caller error */ + /* + * If the tid is not valid, there is no physical row, and all system + * attributes are deemed to be NULL, except for the tableoid. + */ if (attnum == TableOidAttributeNumber) { *isnull = false; return ObjectIdGetDatum(slot->tts_tableOid); } - else if (attnum == SelfItemPointerAttributeNumber) + if (!ItemPointerIsValid(&slot->tts_tid)) + { + *isnull = true; + return PointerGetDatum(NULL); + } + if (attnum == SelfItemPointerAttributeNumber) { *isnull = false; return PointerGetDatum(&slot->tts_tid); diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h new file mode 100644 index 5d7f17d..934815d --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -280,6 +280,12 @@ typedef struct ExprContext #define FIELDNO_EXPRCONTEXT_DOMAINNULL 13 bool domainValue_isNull; + /* Tuples that OLD/NEW Var nodes in RETURNING may refer to */ +#define FIELDNO_EXPRCONTEXT_OLDTUPLE 14 + TupleTableSlot *ecxt_oldtuple; +#define FIELDNO_EXPRCONTEXT_NEWTUPLE 15 + TupleTableSlot *ecxt_newtuple; + /* Link to containing EState (NULL if a standalone ExprContext) */ struct EState *ecxt_estate; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h new file mode 100644 index e494309..f9deabd --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -185,6 +185,8 @@ typedef struct Query OnConflictExpr *onConflict; /* ON CONFLICT DO [NOTHING | UPDATE] */ + char *returningOld; /* alias for OLD in RETURNING list */ + char *returningNew; /* alias for NEW in RETURNING list */ List *returningList; /* return-values list (of TargetEntry) */ List *groupClause; /* a list of SortGroupClause's */ @@ -1675,6 +1677,32 @@ typedef struct MergeWhenClause } MergeWhenClause; /* + * ReturningOption - + * Option in RETURNING WITH(...) list + * + * Currently, this is used only for specifying the OLD/NEW aliases available + * for use in the RETURNING expression list. + */ +typedef struct ReturningOption +{ + NodeTag type; + bool isNew; + char *name; + int location; +} ReturningOption; + +/* + * ReturningClause - + * List of RETURNING expressions, together with any WITH(...) options + */ +typedef struct ReturningClause +{ + NodeTag type; + List *options; /* list of ReturningOption elements */ + List *exprs; /* list of expressions to return */ +} ReturningClause; + +/* * TriggerTransition - * representation of transition row or table naming clause * @@ -1882,7 +1910,7 @@ typedef struct InsertStmt List *cols; /* optional: names of the target columns */ Node *selectStmt; /* the source SELECT/VALUES, or NULL */ OnConflictClause *onConflictClause; /* ON CONFLICT clause */ - List *returningList; /* list of expressions to return */ + ReturningClause *returningClause; /* RETURNING clause */ WithClause *withClause; /* WITH clause */ OverridingKind override; /* OVERRIDING clause */ } InsertStmt; @@ -1897,7 +1925,7 @@ typedef struct DeleteStmt RangeVar *relation; /* relation to delete from */ List *usingClause; /* optional using clause for more tables */ Node *whereClause; /* qualifications */ - List *returningList; /* list of expressions to return */ + ReturningClause *returningClause; /* RETURNING clause */ WithClause *withClause; /* WITH clause */ } DeleteStmt; @@ -1912,7 +1940,7 @@ typedef struct UpdateStmt List *targetList; /* the target list (of ResTarget) */ Node *whereClause; /* qualifications */ List *fromClause; /* optional from clause for more tables */ - List *returningList; /* list of expressions to return */ + ReturningClause *returningClause; /* RETURNING clause */ WithClause *withClause; /* WITH clause */ } UpdateStmt; diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h new file mode 100644 index bb930af..2022208 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -209,6 +209,11 @@ typedef struct Expr * Note that it affects the meaning of all of varno, varnullingrels, and * varnosyn, all of which refer to the range table of that query level. * + * varreturningtype is used for Vars in the RETURNING list of data-modifying + * queries, for Vars that refer to the target relation. For such Vars, there + * are 3 possible behaviors, depending on whether the target relation was + * referred to directly, or via the OLD or NEW aliases. + * * In the parser, varnosyn and varattnosyn are either identical to * varno/varattno, or they specify the column's position in an aliased JOIN * RTE that hides the semantic referent RTE's refname. This is a syntactic @@ -230,6 +235,14 @@ typedef struct Expr #define PRS2_OLD_VARNO 1 #define PRS2_NEW_VARNO 2 +/* Returning behavior for Vars in RETURNING list */ +typedef enum VarReturningType +{ + VAR_RETURNING_DEFAULT, /* return OLD for DELETE, else return NEW */ + VAR_RETURNING_OLD, /* return OLD for DELETE/UPDATE, else NULL */ + VAR_RETURNING_NEW, /* return NEW for INSERT/UPDATE, else NULL */ +} VarReturningType; + typedef struct Var { Expr xpr; @@ -265,6 +278,9 @@ typedef struct Var */ Index varlevelsup; + /* returning type of this var (see above) */ + VarReturningType varreturningtype; + /* * varnosyn/varattnosyn are ignored for equality, because Vars with * different syntactic identifiers are semantically the same as long as diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h new file mode 100644 index f589112..18483e8 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -175,6 +175,10 @@ typedef Node *(*CoerceParamHook) (ParseS * p_resolve_unknowns: resolve unknown-type SELECT output columns as type TEXT * (this is true by default). * + * p_returning_old: alias for OLD in RETURNING list, or NULL. + * + * p_returning_new: alias for NEW in RETURNING list, or NULL. + * * p_hasAggs, p_hasWindowFuncs, etc: true if we've found any of the indicated * constructs in the query. * @@ -215,6 +219,8 @@ struct ParseState * with FOR UPDATE/FOR SHARE */ bool p_resolve_unknowns; /* resolve unknown-type SELECT outputs as * type text */ + char *p_returning_old; /* alias for OLD in RETURNING list */ + char *p_returning_new; /* alias for NEW in RETURNING list */ QueryEnvironment *p_queryEnv; /* curr env, incl refs to enclosing env */ @@ -275,6 +281,11 @@ struct ParseState * of SQL:2008 requires us to do it this way. We also use p_lateral_ok to * forbid LATERAL references to an UPDATE/DELETE target table. * + * While processing the RETURNING clause, special namespace items are added to + * refer to the OLD and NEW state of the result relation. These namespace + * items have p_returning_type set appropriately, for use when creating Vars. + * For convenience, this information is duplicated on each namespace column. + * * At no time should a namespace list contain two entries that conflict * according to the rules in checkNameSpaceConflicts; but note that those * are more complicated than "must have different alias names", so in practice @@ -292,6 +303,7 @@ struct ParseNamespaceItem bool p_cols_visible; /* Column names visible as unqualified refs? */ bool p_lateral_only; /* Is only visible to LATERAL expressions? */ bool p_lateral_ok; /* If so, does join type allow use? */ + VarReturningType p_returning_type; /* Is OLD/NEW for use in RETURNING? */ }; /* @@ -322,6 +334,7 @@ struct ParseNamespaceColumn Oid p_vartype; /* pg_type OID */ int32 p_vartypmod; /* type modifier value */ Oid p_varcollid; /* OID of collation, or InvalidOid */ + VarReturningType p_varreturningtype; /* for RETURNING OLD/NEW */ Index p_varnosyn; /* rangetable index of syntactic referent */ AttrNumber p_varattnosyn; /* attribute number of syntactic referent */ bool p_dontexpand; /* not included in star expansion */ diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h new file mode 100644 index 67d9b1e..55355ea --- a/src/include/parser/parse_relation.h +++ b/src/include/parser/parse_relation.h @@ -112,6 +112,7 @@ extern void errorMissingRTE(ParseState * extern void errorMissingColumn(ParseState *pstate, const char *relname, const char *colname, int location) pg_attribute_noreturn(); extern void expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up, + VarReturningType returning_type, int location, bool include_dropped, List **colnames, List **colvars); extern List *expandNSItemVars(ParseState *pstate, ParseNamespaceItem *nsitem, diff --git a/src/include/rewrite/rewriteManip.h b/src/include/rewrite/rewriteManip.h new file mode 100644 index ca12780..036f2de --- a/src/include/rewrite/rewriteManip.h +++ b/src/include/rewrite/rewriteManip.h @@ -93,4 +93,12 @@ extern Node *ReplaceVarsFromTargetList(N int nomatch_varno, bool *outer_hasSubLinks); +extern Node *ReplaceReturningVarsFromTargetList(Node *node, + int target_varno, + int sublevels_up, + RangeTblEntry *target_rte, + List *targetlist, + int new_result_relation, + bool *outer_hasSubLinks); + #endif /* REWRITEMANIP_H */ diff --git a/src/interfaces/ecpg/preproc/parse.pl b/src/interfaces/ecpg/preproc/parse.pl new file mode 100644 index 7574fc3..584b8ae --- a/src/interfaces/ecpg/preproc/parse.pl +++ b/src/interfaces/ecpg/preproc/parse.pl @@ -118,8 +118,8 @@ my %replace_line = ( 'SHOW TRANSACTION ISOLATION LEVEL ecpg_into', 'VariableShowStmtSHOWSESSIONAUTHORIZATION' => 'SHOW SESSION AUTHORIZATION ecpg_into', - 'returning_clauseRETURNINGtarget_list' => - 'RETURNING target_list opt_ecpg_into', + 'returning_clauseRETURNINGreturning_with_clausetarget_list' => + 'RETURNING returning_with_clause target_list opt_ecpg_into', 'ExecuteStmtEXECUTEnameexecute_param_clause' => 'EXECUTE prepared_name execute_param_clause execute_rest', 'ExecuteStmtCREATEOptTempTABLEcreate_as_targetASEXECUTEnameexecute_param_clauseopt_with_data' diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out new file mode 100644 index cb51bb8..648d05f --- a/src/test/regress/expected/returning.out +++ b/src/test/regress/expected/returning.out @@ -355,3 +355,210 @@ INSERT INTO foo AS bar DEFAULT VALUES RE 42 (1 row) +-- +-- Test RETURNING OLD/NEW. +-- +-- Start with new data, to ensure predictable TIDs. +-- +TRUNCATE foo; +INSERT INTO foo VALUES (1, 'xxx', 10, 20), (2, 'more', 42, 141), (3, 'zoo2', 57, 99); +-- Error cases +INSERT INTO foo DEFAULT VALUES RETURNING WITH (nonsuch AS something) *; +ERROR: syntax error at or near "nonsuch" +LINE 1: INSERT INTO foo DEFAULT VALUES RETURNING WITH (nonsuch AS so... + ^ +INSERT INTO foo DEFAULT VALUES RETURNING WITH (new AS foo) *; +ERROR: table name "foo" specified more than once +LINE 1: INSERT INTO foo DEFAULT VALUES RETURNING WITH (new AS foo) *... + ^ +INSERT INTO foo DEFAULT VALUES RETURNING WITH (old AS o, new AS n, old AS o) *; +ERROR: OLD cannot be specified multiple times +LINE 1: ...EFAULT VALUES RETURNING WITH (old AS o, new AS n, old AS o) ... + ^ +-- INSERT has NEW, but not OLD +INSERT INTO foo VALUES (4) + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + tableoid | ctid | f1 | f2 | f3 | f4 | tableoid | ctid | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 +----------+------+----+----+----+----+----------+-------+----+----+----+----+----+----+----+---- + foo | | | | | | foo | (0,4) | 4 | | 42 | 99 | 4 | | 42 | 99 +(1 row) + +-- INSERT ... ON CONFLICT ... UPDATE has OLD and NEW +CREATE UNIQUE INDEX foo_f1_idx ON foo (f1); +INSERT INTO foo VALUES (4, 'conflict'), (5, 'ok') + ON CONFLICT (f1) DO UPDATE SET f2 = excluded.f2||'ed', f3 = -1 + RETURNING WITH (OLD AS o, NEW AS n) + o.tableoid::regclass, o.ctid, o.*, + n.tableoid::regclass, n.ctid, n.*, *; + tableoid | ctid | f1 | f2 | f3 | f4 | tableoid | ctid | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 +----------+-------+----+----+----+----+----------+-------+----+------------+----+----+----+------------+----+---- + foo | (0,4) | 4 | | 42 | 99 | foo | (0,5) | 4 | conflicted | -1 | 99 | 4 | conflicted | -1 | 99 + foo | | | | | | foo | (0,6) | 5 | ok | 42 | 99 | 5 | ok | 42 | 99 +(2 rows) + +-- UPDATE has OLD and NEW +UPDATE foo SET f4 = 100 WHERE f1 = 5 + RETURNING old.tableoid::regclass, old.ctid, old.*, old, + new.tableoid::regclass, new.ctid, new.*, new, + old.f4::text||'->'||new.f4::text AS change; + tableoid | ctid | f1 | f2 | f3 | f4 | old | tableoid | ctid | f1 | f2 | f3 | f4 | new | change +----------+-------+----+----+----+----+--------------+----------+-------+----+----+----+-----+---------------+--------- + foo | (0,6) | 5 | ok | 42 | 99 | (5,ok,42,99) | foo | (0,7) | 5 | ok | 42 | 100 | (5,ok,42,100) | 99->100 +(1 row) + +-- DELETE has OLD, but not NEW +DELETE FROM foo WHERE f1 = 5 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + tableoid | ctid | f1 | f2 | f3 | f4 | tableoid | ctid | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 +----------+-------+----+----+----+-----+----------+------+----+----+----+----+----+----+----+----- + foo | (0,7) | 5 | ok | 42 | 100 | foo | | | | | | 5 | ok | 42 | 100 +(1 row) + +-- DELETE turned into UPDATE by a rule has OLD and NEW +CREATE RULE foo_del_rule AS ON DELETE TO foo DO INSTEAD + UPDATE foo SET f2 = f2||' (deleted)', f3 = -1, f4 = -1 WHERE f1 = OLD.f1 + RETURNING *; +DELETE FROM foo WHERE f1 = 4 RETURNING old.*,new.*, *; + f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 | f1 | f2 | f3 | f4 +----+------------+----+----+----+----------------------+----+----+----+----------------------+----+---- + 4 | conflicted | -1 | 99 | 4 | conflicted (deleted) | -1 | -1 | 4 | conflicted (deleted) | -1 | -1 +(1 row) + +-- UPDATE on view with rule +UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57 + RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3; + f1 | f2 | f3 | f4 | other | f1 | f2 | f3 | f4 | other | f1 | f2 | f3 | f4 | other | delta_f3 +----+------+----+----+-------+----+------+----+----+-------+----+------+----+----+-------+---------- + 3 | zoo2 | 57 | 99 | 54321 | 3 | zoo2 | 58 | 99 | 54321 | 3 | zoo2 | 58 | 99 | 54321 | 1 +(1 row) + +-- UPDATE on view with INSTEAD OF trigger +CREATE FUNCTION joinview_upd_trig_fn() RETURNS trigger +LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE 'UPDATE: % -> %', old, new; + UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10 + FROM joinme WHERE f2 = f2j AND f2 = old.f2 + RETURNING new.f1, new.f4 INTO new.f1, new.f4; -- should fail + RETURN NEW; +END; +$$; +CREATE TRIGGER joinview_upd_trig INSTEAD OF UPDATE ON joinview + FOR EACH ROW EXECUTE FUNCTION joinview_upd_trig_fn(); +DROP RULE joinview_u ON joinview; +UPDATE joinview SET f3 = f3 + 1, f4 = 7 WHERE f3 = 58 + RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3; -- should fail +NOTICE: UPDATE: (3,zoo2,58,99,54321) -> (3,zoo2,59,7,54321) +ERROR: column reference "new.f1" is ambiguous +LINE 3: RETURNING new.f1, new.f4 + ^ +DETAIL: It could refer to either a PL/pgSQL variable or a table column. +QUERY: UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10 + FROM joinme WHERE f2 = f2j AND f2 = old.f2 + RETURNING new.f1, new.f4 +CONTEXT: PL/pgSQL function joinview_upd_trig_fn() line 4 at SQL statement +CREATE OR REPLACE FUNCTION joinview_upd_trig_fn() RETURNS trigger +LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE 'UPDATE: % -> %', old, new; + UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10 + FROM joinme WHERE f2 = f2j AND f2 = old.f2 + RETURNING WITH (new AS n) new.f1, n.f4 INTO new.f1, new.f4; -- now ok + RETURN NEW; +END; +$$; +UPDATE joinview SET f3 = f3 + 1, f4 = 7 WHERE f3 = 58 + RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3; -- should succeed +NOTICE: UPDATE: (3,zoo2,58,99,54321) -> (3,zoo2,59,7,54321) + f1 | f2 | f3 | f4 | other | f1 | f2 | f3 | f4 | other | f1 | f2 | f3 | f4 | other | delta_f3 +----+------+----+----+-------+----+------+----+----+-------+----+------+----+----+-------+---------- + 3 | zoo2 | 58 | 99 | 54321 | 3 | zoo2 | 59 | 70 | 54321 | 3 | zoo2 | 59 | 70 | 54321 | 1 +(1 row) + +-- INSERT/DELETE on zero column table +CREATE TABLE zerocol(); +INSERT INTO zerocol SELECT + RETURNING old.tableoid::regclass, old.ctid, + new.tableoid::regclass, new.ctid, ctid, *; + tableoid | ctid | tableoid | ctid | ctid +----------+------+----------+-------+------- + zerocol | | zerocol | (0,1) | (0,1) +(1 row) + +DELETE FROM zerocol + RETURNING old.tableoid::regclass, old.ctid, + new.tableoid::regclass, new.ctid, ctid, *; + tableoid | ctid | tableoid | ctid | ctid +----------+-------+----------+------+------- + zerocol | (0,1) | zerocol | | (0,1) +(1 row) + +DROP TABLE zerocol; +-- Test cross-partition updates and attribute mapping +CREATE TABLE foo_parted (a int, b float8, c text) PARTITION BY LIST (a); +CREATE TABLE foo_part_s1 PARTITION OF foo_parted FOR VALUES IN (1); +CREATE TABLE foo_part_s2 PARTITION OF foo_parted FOR VALUES IN (2); +CREATE TABLE foo_part_d1 (c text, a int, b float8); +ALTER TABLE foo_parted ATTACH PARTITION foo_part_d1 FOR VALUES IN (3); +CREATE TABLE foo_part_d2 (b float8, c text, a int); +ALTER TABLE foo_parted ATTACH PARTITION foo_part_d2 FOR VALUES IN (4); +INSERT INTO foo_parted + VALUES (1, 17.1, 'P1'), (2, 17.2, 'P2'), (3, 17.3, 'P3'), (4, 17.4, 'P4') + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + tableoid | ctid | a | b | c | tableoid | ctid | a | b | c | a | b | c +-------------+------+---+---+---+-------------+-------+---+------+----+---+------+---- + foo_part_s1 | | | | | foo_part_s1 | (0,1) | 1 | 17.1 | P1 | 1 | 17.1 | P1 + foo_part_s2 | | | | | foo_part_s2 | (0,1) | 2 | 17.2 | P2 | 2 | 17.2 | P2 + foo_part_d1 | | | | | foo_part_d1 | (0,1) | 3 | 17.3 | P3 | 3 | 17.3 | P3 + foo_part_d2 | | | | | foo_part_d2 | (0,1) | 4 | 17.4 | P4 | 4 | 17.4 | P4 +(4 rows) + +UPDATE foo_parted SET a = 2, b = b + 1, c = c || '->P2' WHERE a = 1 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + tableoid | ctid | a | b | c | tableoid | ctid | a | b | c | a | b | c +-------------+-------+---+------+----+-------------+-------+---+------+--------+---+------+-------- + foo_part_s1 | (0,1) | 1 | 17.1 | P1 | foo_part_s2 | (0,2) | 2 | 18.1 | P1->P2 | 2 | 18.1 | P1->P2 +(1 row) + +UPDATE foo_parted SET a = 1, b = b + 1, c = c || '->P1' WHERE a = 3 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + tableoid | ctid | a | b | c | tableoid | ctid | a | b | c | a | b | c +-------------+-------+---+------+----+-------------+-------+---+------+--------+---+------+-------- + foo_part_d1 | (0,1) | 3 | 17.3 | P3 | foo_part_s1 | (0,2) | 1 | 18.3 | P3->P1 | 1 | 18.3 | P3->P1 +(1 row) + +UPDATE foo_parted SET a = 3, b = b + 1, c = c || '->P3' WHERE a = 1 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + tableoid | ctid | a | b | c | tableoid | ctid | a | b | c | a | b | c +-------------+-------+---+------+--------+-------------+-------+---+------+------------+---+------+------------ + foo_part_s1 | (0,2) | 1 | 18.3 | P3->P1 | foo_part_d1 | (0,2) | 3 | 19.3 | P3->P1->P3 | 3 | 19.3 | P3->P1->P3 +(1 row) + +UPDATE foo_parted SET a = 4, b = b + 1, c = c || '->P4' WHERE a = 3 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + tableoid | ctid | a | b | c | tableoid | ctid | a | b | c | a | b | c +-------------+-------+---+------+------------+-------------+-------+---+------+----------------+---+------+---------------- + foo_part_d1 | (0,2) | 3 | 19.3 | P3->P1->P3 | foo_part_d2 | (0,2) | 4 | 20.3 | P3->P1->P3->P4 | 4 | 20.3 | P3->P1->P3->P4 +(1 row) + +DELETE FROM foo_parted + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + tableoid | ctid | a | b | c | tableoid | ctid | a | b | c | a | b | c +-------------+-------+---+------+----------------+-------------+------+---+---+---+---+------+---------------- + foo_part_s2 | (0,1) | 2 | 17.2 | P2 | foo_part_s2 | | | | | 2 | 17.2 | P2 + foo_part_s2 | (0,2) | 2 | 18.1 | P1->P2 | foo_part_s2 | | | | | 2 | 18.1 | P1->P2 + foo_part_d2 | (0,1) | 4 | 17.4 | P4 | foo_part_d2 | | | | | 4 | 17.4 | P4 + foo_part_d2 | (0,2) | 4 | 20.3 | P3->P1->P3->P4 | foo_part_d2 | | | | | 4 | 20.3 | P3->P1->P3->P4 +(4 rows) + +DROP TABLE foo_parted; diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql new file mode 100644 index a460f82..6167ec4 --- a/src/test/regress/sql/returning.sql +++ b/src/test/regress/sql/returning.sql @@ -160,3 +160,128 @@ INSERT INTO foo AS bar DEFAULT VALUES RE INSERT INTO foo AS bar DEFAULT VALUES RETURNING foo.*; -- fails, wrong name INSERT INTO foo AS bar DEFAULT VALUES RETURNING bar.*; -- ok INSERT INTO foo AS bar DEFAULT VALUES RETURNING bar.f3; -- ok + +-- +-- Test RETURNING OLD/NEW. +-- +-- Start with new data, to ensure predictable TIDs. +-- +TRUNCATE foo; +INSERT INTO foo VALUES (1, 'xxx', 10, 20), (2, 'more', 42, 141), (3, 'zoo2', 57, 99); + +-- Error cases +INSERT INTO foo DEFAULT VALUES RETURNING WITH (nonsuch AS something) *; +INSERT INTO foo DEFAULT VALUES RETURNING WITH (new AS foo) *; +INSERT INTO foo DEFAULT VALUES RETURNING WITH (old AS o, new AS n, old AS o) *; + +-- INSERT has NEW, but not OLD +INSERT INTO foo VALUES (4) + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + +-- INSERT ... ON CONFLICT ... UPDATE has OLD and NEW +CREATE UNIQUE INDEX foo_f1_idx ON foo (f1); +INSERT INTO foo VALUES (4, 'conflict'), (5, 'ok') + ON CONFLICT (f1) DO UPDATE SET f2 = excluded.f2||'ed', f3 = -1 + RETURNING WITH (OLD AS o, NEW AS n) + o.tableoid::regclass, o.ctid, o.*, + n.tableoid::regclass, n.ctid, n.*, *; + +-- UPDATE has OLD and NEW +UPDATE foo SET f4 = 100 WHERE f1 = 5 + RETURNING old.tableoid::regclass, old.ctid, old.*, old, + new.tableoid::regclass, new.ctid, new.*, new, + old.f4::text||'->'||new.f4::text AS change; + +-- DELETE has OLD, but not NEW +DELETE FROM foo WHERE f1 = 5 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + +-- DELETE turned into UPDATE by a rule has OLD and NEW +CREATE RULE foo_del_rule AS ON DELETE TO foo DO INSTEAD + UPDATE foo SET f2 = f2||' (deleted)', f3 = -1, f4 = -1 WHERE f1 = OLD.f1 + RETURNING *; +DELETE FROM foo WHERE f1 = 4 RETURNING old.*,new.*, *; + +-- UPDATE on view with rule +UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57 + RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3; + +-- UPDATE on view with INSTEAD OF trigger +CREATE FUNCTION joinview_upd_trig_fn() RETURNS trigger +LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE 'UPDATE: % -> %', old, new; + UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10 + FROM joinme WHERE f2 = f2j AND f2 = old.f2 + RETURNING new.f1, new.f4 INTO new.f1, new.f4; -- should fail + RETURN NEW; +END; +$$; +CREATE TRIGGER joinview_upd_trig INSTEAD OF UPDATE ON joinview + FOR EACH ROW EXECUTE FUNCTION joinview_upd_trig_fn(); +DROP RULE joinview_u ON joinview; +UPDATE joinview SET f3 = f3 + 1, f4 = 7 WHERE f3 = 58 + RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3; -- should fail + +CREATE OR REPLACE FUNCTION joinview_upd_trig_fn() RETURNS trigger +LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE 'UPDATE: % -> %', old, new; + UPDATE foo SET f1 = new.f1, f3 = new.f3, f4 = new.f4 * 10 + FROM joinme WHERE f2 = f2j AND f2 = old.f2 + RETURNING WITH (new AS n) new.f1, n.f4 INTO new.f1, new.f4; -- now ok + RETURN NEW; +END; +$$; +UPDATE joinview SET f3 = f3 + 1, f4 = 7 WHERE f3 = 58 + RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3; -- should succeed + +-- INSERT/DELETE on zero column table +CREATE TABLE zerocol(); +INSERT INTO zerocol SELECT + RETURNING old.tableoid::regclass, old.ctid, + new.tableoid::regclass, new.ctid, ctid, *; +DELETE FROM zerocol + RETURNING old.tableoid::regclass, old.ctid, + new.tableoid::regclass, new.ctid, ctid, *; +DROP TABLE zerocol; + +-- Test cross-partition updates and attribute mapping +CREATE TABLE foo_parted (a int, b float8, c text) PARTITION BY LIST (a); +CREATE TABLE foo_part_s1 PARTITION OF foo_parted FOR VALUES IN (1); +CREATE TABLE foo_part_s2 PARTITION OF foo_parted FOR VALUES IN (2); +CREATE TABLE foo_part_d1 (c text, a int, b float8); +ALTER TABLE foo_parted ATTACH PARTITION foo_part_d1 FOR VALUES IN (3); +CREATE TABLE foo_part_d2 (b float8, c text, a int); +ALTER TABLE foo_parted ATTACH PARTITION foo_part_d2 FOR VALUES IN (4); + +INSERT INTO foo_parted + VALUES (1, 17.1, 'P1'), (2, 17.2, 'P2'), (3, 17.3, 'P3'), (4, 17.4, 'P4') + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + +UPDATE foo_parted SET a = 2, b = b + 1, c = c || '->P2' WHERE a = 1 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + +UPDATE foo_parted SET a = 1, b = b + 1, c = c || '->P1' WHERE a = 3 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + +UPDATE foo_parted SET a = 3, b = b + 1, c = c || '->P3' WHERE a = 1 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + +UPDATE foo_parted SET a = 4, b = b + 1, c = c || '->P4' WHERE a = 3 + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + +DELETE FROM foo_parted + RETURNING old.tableoid::regclass, old.ctid, old.*, + new.tableoid::regclass, new.ctid, new.*, *; + +DROP TABLE foo_parted; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list new file mode 100644 index d659adb..c4b0b0f --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2350,6 +2350,7 @@ ReorderBufferUpdateProgressTxnCB ReorderTuple RepOriginId ReparameterizeForeignPathByChild_function +ReplaceReturningVarsFromTargetList_context ReplaceVarsFromTargetList_context ReplaceVarsNoMatchOption ReplicaIdentityStmt @@ -2379,6 +2380,8 @@ RestrictInfo Result ResultRelInfo ResultState +ReturningClause +ReturningOption ReturnSetInfo ReturnStmt RevmapContents @@ -2521,6 +2524,7 @@ SetOperationStmt SetQuantifier SetToDefault SetupWorkerPtrType +SetVarReturningType_context ShDependObjectInfo SharedAggInfo SharedBitmapState @@ -2970,6 +2974,7 @@ VariableSpace VariableStatData VariableSubstituteHook Variables +VarReturningType Vector32 Vector8 VersionedQuery