From 6988adbef4b31c7c2d2c0979c94388ab458418e2 Mon Sep 17 00:00:00 2001 From: Dean Rasheed Date: Fri, 25 Jul 2025 08:57:03 +0100 Subject: [PATCH] Allow EXCLUDED in RETURNING list of INSERT ON CONFLICT DO UPDATE. This allows an INSERT ... ON CONFLICT DO UPDATE command to return the excluded values, when conflicts occur. For rows that do not conflict, the excluded values returned are NULL. This builds on the infrastructure added to support OLD/NEW values in the RETURNING list, except that in the case of EXCLUDED, there is no need for the special syntax to change the name of the excluded pseudo-relation in the RETURNING list. (That was required for OLD/NEW, because those names were already used in some situations, such as trigger functions, but there is no such problem with excluded.) Discussion: https://postgr.es/m/CAEZATCXXu2ohYmn=4YrRQa9yNwD_fdEEOTBPgM_5jhQOFcaQ4g@mail.gmail.com --- doc/src/sgml/dml.sgml | 13 +++ doc/src/sgml/ref/insert.sgml | 42 ++++++--- src/backend/executor/execExpr.c | 44 +++++++-- src/backend/executor/execExprInterp.c | 21 ++++- src/backend/executor/execPartition.c | 14 +++ src/backend/executor/nodeModifyTable.c | 51 +++++++--- src/backend/optimizer/path/allpaths.c | 3 +- src/backend/optimizer/plan/setrefs.c | 77 ++++++++++------ src/backend/optimizer/prep/prepjointree.c | 22 +++-- src/backend/optimizer/prep/preptlist.c | 9 +- src/backend/optimizer/util/clauses.c | 2 +- src/backend/optimizer/util/var.c | 11 ++- src/backend/parser/analyze.c | 17 +++- src/backend/parser/parse_expr.c | 2 +- src/backend/parser/parse_relation.c | 4 +- src/backend/rewrite/rewriteHandler.c | 42 +++++---- src/backend/rewrite/rewriteManip.c | 69 ++++++++------ src/include/executor/execExpr.h | 7 +- src/include/nodes/execnodes.h | 2 + src/include/nodes/primnodes.h | 26 ++++-- src/include/parser/parse_node.h | 7 +- src/include/rewrite/rewriteManip.h | 4 +- src/test/regress/expected/arrays.out | 26 ++++-- .../regress/expected/generated_stored.out | 48 ++++++---- .../regress/expected/generated_virtual.out | 48 ++++++---- src/test/regress/expected/inherit.out | 9 +- src/test/regress/expected/insert_conflict.out | 92 +++++++++++++------ src/test/regress/expected/returning.out | 22 +++-- src/test/regress/expected/rules.out | 61 ++++++++++++ src/test/regress/expected/triggers.out | 74 ++++++++++++--- src/test/regress/expected/updatable_views.out | 27 +++++- src/test/regress/sql/arrays.sql | 9 +- src/test/regress/sql/generated_stored.sql | 4 + src/test/regress/sql/generated_virtual.sql | 4 + src/test/regress/sql/inherit.sql | 3 +- src/test/regress/sql/insert_conflict.sql | 29 +++--- src/test/regress/sql/returning.sql | 8 +- src/test/regress/sql/rules.sql | 27 ++++++ src/test/regress/sql/triggers.sql | 17 ++-- src/test/regress/sql/updatable_views.sql | 12 ++- src/tools/pgindent/typedefs.list | 1 + 41 files changed, 741 insertions(+), 269 deletions(-) diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml index 458aee788b7f..4712a49071b8 100644 --- a/doc/src/sgml/dml.sgml +++ b/doc/src/sgml/dml.sgml @@ -392,6 +392,19 @@ UPDATE products SET price = price * 1.10 the new values may be non-NULL. + + In an INSERT with an ON CONFLICT DO UPDATE + clause, it is also possible to return the values excluded, if there is a + conflict. For example: + +INSERT INTO products AS p SELECT * FROM new_products + ON CONFLICT (product_no) DO UPDATE SET name = excluded.name + RETURNING p.product_no, p.name, excluded.price; + + Excluded values will be NULL for rows that do not + conflict. + + If there are triggers () on the target table, the data available to RETURNING is the row as modified by diff --git a/doc/src/sgml/ref/insert.sgml b/doc/src/sgml/ref/insert.sgml index b337f2ee5558..81724eff38cc 100644 --- a/doc/src/sgml/ref/insert.sgml +++ b/doc/src/sgml/ref/insert.sgml @@ -183,7 +183,7 @@ INSERT INTO table_name [ AS ON CONFLICT DO UPDATE targets a table named excluded, since that will otherwise be taken as the name of the special table representing the row proposed - for insertion. + for insertion (see note below). @@ -343,6 +343,35 @@ INSERT INTO table_name [ AS ON CONFLICT DO UPDATE clause, the old values may be non-NULL. + + + If the INSERT has an ON CONFLICT DO + UPDATE clause, a column name or * may be + qualified using EXCLUDED to return the values + excluded from insertion, in the event of a conflict. If there is no + conflict, then all excluded values will be NULL. + + + + + The special excluded table is made available + whenever an INSERT has an ON CONFLICT DO + UPDATE clause. It has the same columns as the target + table, and in the event of a conflict, it is populated with the + values that would have been inserted. This includes any values that + were supplied by defaults, as well as the effects of any per-row + BEFORE INSERT triggers, since those may have + contributed to the row being excluded from insertion. + + + The excluded table is accessible from the + SET and WHERE clauses of the + ON CONFLICT DO UPDATE action, and the + RETURNING list. SELECT + privilege is required on any columns in the target table where the + corresponding excluded columns are read. + + @@ -440,16 +469,7 @@ INSERT INTO table_name [ AS WHERE clauses in ON CONFLICT DO UPDATE have access to the existing row using the table's name (or an alias), and to the row proposed for insertion - using the special excluded table. - SELECT privilege is required on any column in the - target table where corresponding excluded - columns are read. - - - Note that the effects of all per-row BEFORE - INSERT triggers are reflected in - excluded values, since those effects may - have contributed to the row being excluded from insertion. + using the special excluded table (see note above). diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b529..d13c82bdd301 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -455,6 +455,7 @@ ExecBuildProjectionInfo(List *targetList, /* * Get the tuple from the relation being scanned, or the * old/new tuple slot, if old/new values were requested. + * Should not see EXCLUDED here (should be INNER_VAR). */ switch (variable->varreturningtype) { @@ -469,6 +470,10 @@ ExecBuildProjectionInfo(List *targetList, scratch.opcode = EEOP_ASSIGN_NEW_VAR; state->flags |= EEO_FLAG_HAS_NEW; break; + case VAR_RETURNING_EXCLUDED: + elog(ERROR, "wrong varno %d (expected %d) for variable returning excluded", + variable->varno, INNER_VAR); + break; } break; } @@ -972,6 +977,10 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.opcode = EEOP_NEW_SYSVAR; state->flags |= EEO_FLAG_HAS_NEW; break; + case VAR_RETURNING_EXCLUDED: + elog(ERROR, "wrong varno %d (expected %d) for variable returning excluded", + variable->varno, INNER_VAR); + break; } break; } @@ -1007,6 +1016,10 @@ ExecInitExprRec(Expr *node, ExprState *state, scratch.opcode = EEOP_NEW_VAR; state->flags |= EEO_FLAG_HAS_NEW; break; + case VAR_RETURNING_EXCLUDED: + elog(ERROR, "wrong varno %d (expected %d) for variable returning excluded", + variable->varno, INNER_VAR); + break; } break; } @@ -2638,10 +2651,23 @@ ExecInitExprRec(Expr *node, ExprState *state, ReturningExpr *rexpr = (ReturningExpr *) node; int retstep; - /* Skip expression evaluation if OLD/NEW row doesn't exist */ + /* + * Skip expression evaluation if OLD/NEW/EXCLUDED row doesn't + * exist. + */ scratch.opcode = EEOP_RETURNINGEXPR; - scratch.d.returningexpr.nullflag = rexpr->retold ? - EEO_FLAG_OLD_IS_NULL : EEO_FLAG_NEW_IS_NULL; + switch (rexpr->retkind) + { + case RETURNING_OLD_EXPR: + scratch.d.returningexpr.nullflag = EEO_FLAG_OLD_IS_NULL; + break; + case RETURNING_NEW_EXPR: + scratch.d.returningexpr.nullflag = EEO_FLAG_NEW_IS_NULL; + break; + case RETURNING_EXCLUDED_EXPR: + scratch.d.returningexpr.nullflag = EEO_FLAG_INNER_IS_NULL; + break; + } scratch.d.returningexpr.jumpdone = -1; /* set below */ ExprEvalPushStep(state, &scratch); retstep = state->steps_len - 1; @@ -2649,14 +2675,15 @@ ExecInitExprRec(Expr *node, ExprState *state, /* Steps to evaluate expression to return */ ExecInitExprRec(rexpr->retexpr, state, resv, resnull); - /* Jump target used if OLD/NEW row doesn't exist */ + /* Jump target used if OLD/NEW/EXCLUDED row doesn't exist */ state->steps[retstep].d.returningexpr.jumpdone = state->steps_len; /* Update ExprState flags */ - if (rexpr->retold) + if (rexpr->retkind == RETURNING_OLD_EXPR) state->flags |= EEO_FLAG_HAS_OLD; - else + else if (rexpr->retkind == RETURNING_NEW_EXPR) state->flags |= EEO_FLAG_HAS_NEW; + /* we don't bother recording references to EXCLUDED */ break; } @@ -3013,6 +3040,10 @@ expr_setup_walker(Node *node, ExprSetupInfo *info) case VAR_RETURNING_NEW: info->last_new = Max(info->last_new, attnum); break; + case VAR_RETURNING_EXCLUDED: + elog(ERROR, "wrong varno %d (expected %d) for variable returning excluded", + variable->varno, INNER_VAR); + break; } break; } @@ -3178,6 +3209,7 @@ ExecInitWholeRowVar(ExprEvalStep *scratch, Var *variable, ExprState *state) state->flags |= EEO_FLAG_HAS_OLD; else if (variable->varreturningtype == VAR_RETURNING_NEW) state->flags |= EEO_FLAG_HAS_NEW; + /* we don't bother recording references to EXCLUDED */ /* * If the input tuple came from a subquery, it might contain "resjunk" diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 0e1a74976f7d..24fc1b292a5d 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -5339,7 +5339,21 @@ ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext) switch (variable->varno) { case INNER_VAR: - /* get the tuple from the inner node */ + + /* + * Get the tuple from the inner node. + * + * In a RETURNING expression, this is used for EXCLUDED values in + * an INSERT ... ON CONFLICT DO UPDATE. If the non-conflicting + * branch is taken, the EXCLUDED row is NULL, and we return NULL + * rather than an all-NULL record. + */ + if (state->flags & EEO_FLAG_INNER_IS_NULL) + { + *op->resvalue = (Datum) 0; + *op->resnull = true; + return; + } slot = econtext->ecxt_innertuple; break; @@ -5384,6 +5398,11 @@ ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op, ExprContext *econtext) } slot = econtext->ecxt_newtuple; break; + + case VAR_RETURNING_EXCLUDED: + elog(ERROR, "wrong varno %d (expected %d) for variable returning excluded", + variable->varno, INNER_VAR); + break; } break; } diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c index aa12e9ad2ea8..f473161d0898 100644 --- a/src/backend/executor/execPartition.c +++ b/src/backend/executor/execPartition.c @@ -647,12 +647,26 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate, /* * Convert Vars in it to contain this partition's attribute numbers. + * If we're doing an INSERT ... ON CONFLICT DO UPDATE, the RETURNING + * list might contain references to the EXCLUDED pseudo-relation + * (INNER_VAR), so we must map their attribute numbers too. */ if (part_attmap == NULL) part_attmap = build_attrmap_by_name(RelationGetDescr(partrel), RelationGetDescr(firstResultRel), false); + + if (node->onConflictAction == ONCONFLICT_UPDATE) + { + returningList = (List *) + map_variable_attnos((Node *) returningList, + INNER_VAR, 0, + part_attmap, + RelationGetForm(partrel)->reltype, + &found_whole_row); + /* We ignore the value of found_whole_row. */ + } returningList = (List *) map_variable_attnos((Node *) returningList, firstVarno, 0, diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 4c5647ac38a1..f2c3f32d5231 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -277,6 +277,7 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList) * 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 + * exclSlot: slot holding EXCLUDED tuple (for INSERT ... ON CONFLICT ...) * * Note: If oldSlot and newSlot are NULL, the FDW should have already provided * econtext's scan tuple and its old & new tuples are not needed (FDW direct- @@ -290,8 +291,11 @@ ExecProcessReturning(ModifyTableContext *context, CmdType cmdType, TupleTableSlot *oldSlot, TupleTableSlot *newSlot, - TupleTableSlot *planSlot) + TupleTableSlot *planSlot, + TupleTableSlot *exclSlot) { + ModifyTableState *mtstate = context->mtstate; + ModifyTable *node = (ModifyTable *) mtstate->ps.plan; EState *estate = context->estate; ProjectionInfo *projectReturning = resultRelInfo->ri_projectReturning; ExprContext *econtext = projectReturning->pi_exprContext; @@ -332,10 +336,18 @@ ExecProcessReturning(ModifyTableContext *context, else econtext->ecxt_newtuple = NULL; /* No references to NEW columns */ + /* Make EXCLUDED tuple available to ExecProject, if required */ + if (exclSlot) + econtext->ecxt_innertuple = exclSlot; + else if (cmdType == CMD_INSERT && node->onConflictAction == ONCONFLICT_UPDATE) + econtext->ecxt_innertuple = ExecGetAllNullSlot(estate, resultRelInfo); + else + econtext->ecxt_innertuple = NULL; + /* - * Tell ExecProject whether or not the OLD/NEW rows actually exist. This - * information is required to evaluate ReturningExpr nodes and also in - * ExecEvalSysVar() and ExecEvalWholeRowVar(). + * Tell ExecProject whether or not the OLD/NEW/EXCLUDED rows actually + * exist. This information is required to evaluate ReturningExpr nodes + * and also in ExecEvalSysVar() and ExecEvalWholeRowVar(). */ if (oldSlot == NULL) projectReturning->pi_state.flags |= EEO_FLAG_OLD_IS_NULL; @@ -347,6 +359,11 @@ ExecProcessReturning(ModifyTableContext *context, else projectReturning->pi_state.flags &= ~EEO_FLAG_NEW_IS_NULL; + if (exclSlot == NULL) + projectReturning->pi_state.flags |= EEO_FLAG_INNER_IS_NULL; + else + projectReturning->pi_state.flags &= ~EEO_FLAG_INNER_IS_NULL; + /* Compute the RETURNING expressions */ return ExecProject(projectReturning); } @@ -1330,7 +1347,7 @@ ExecInsert(ModifyTableContext *context, } result = ExecProcessReturning(context, resultRelInfo, CMD_INSERT, - oldSlot, slot, planSlot); + oldSlot, slot, planSlot, NULL); /* * For a cross-partition UPDATE, release the old tuple, first making @@ -1891,7 +1908,7 @@ ExecDelete(ModifyTableContext *context, } rslot = ExecProcessReturning(context, resultRelInfo, CMD_DELETE, - slot, NULL, context->planSlot); + slot, NULL, context->planSlot, NULL); /* * Before releasing the target tuple again, make sure rslot has a @@ -2451,6 +2468,8 @@ ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context, * planSlot is the output of the ModifyTable's subplan; we use it * to access values from other input tables (for RETURNING), * row-ID junk columns, etc. + * exclSlot contains the EXCLUDED tuple if this is the auxiliary + * UPDATE of an INSERT ... ON CONFLICT DO UPDATE. * * Returns RETURNING result if any, otherwise NULL. On exit, if tupleid * had identified the tuple to update, it will identify the tuple @@ -2460,7 +2479,7 @@ ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context, static TupleTableSlot * ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *oldSlot, - TupleTableSlot *slot, bool canSetTag) + TupleTableSlot *slot, TupleTableSlot *exclSlot, bool canSetTag) { EState *estate = context->estate; Relation resultRelationDesc = resultRelInfo->ri_RelationDesc; @@ -2693,7 +2712,8 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) return ExecProcessReturning(context, resultRelInfo, CMD_UPDATE, - oldSlot, slot, context->planSlot); + oldSlot, slot, context->planSlot, + exclSlot); return NULL; } @@ -2915,6 +2935,7 @@ ExecOnConflictUpdate(ModifyTableContext *context, *returning = ExecUpdate(context, resultRelInfo, conflictTid, NULL, existing, resultRelInfo->ri_onConflict->oc_ProjSlot, + excludedSlot, canSetTag); /* @@ -3540,7 +3561,8 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, CMD_UPDATE, resultRelInfo->ri_oldTupleSlot, newslot, - context->planSlot); + context->planSlot, + NULL); break; case CMD_DELETE: @@ -3549,7 +3571,8 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo, CMD_DELETE, resultRelInfo->ri_oldTupleSlot, NULL, - context->planSlot); + context->planSlot, + NULL); break; case CMD_NOTHING: @@ -4313,12 +4336,16 @@ ExecModifyTable(PlanState *pstate) * provide it here. The individual old and new slots are not * needed, since direct-modify is disabled if the RETURNING list * refers to OLD/NEW values. + * + * Currently, foreign tables do not support UNIQUE constraints, + * and therefore they do not support INSERT ... ON CONFLICT, and + * so the EXCLUDED slot is also not needed. */ Assert((resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD) == 0 && (resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_NEW) == 0); slot = ExecProcessReturning(&context, resultRelInfo, operation, - NULL, NULL, context.planSlot); + NULL, NULL, context.planSlot, NULL); return slot; } @@ -4508,7 +4535,7 @@ ExecModifyTable(PlanState *pstate) /* Now apply the update. */ slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple, - oldSlot, slot, node->canSetTag); + oldSlot, slot, NULL, node->canSetTag); if (tuplock) UnlockTuple(resultRelInfo->ri_RelationDesc, tupleid, InplaceUpdateTupleLock); diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 9c6436eb72f2..267cf584fbab 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -4458,8 +4458,7 @@ subquery_push_qual(Query *subquery, RangeTblEntry *rte, Index rti, Node *qual) * each component query gets its own copy of the qual. */ qual = ReplaceVarsFromTargetList(qual, rti, 0, rte, - subquery->targetList, - subquery->resultRelation, + subquery->targetList, 0, REPLACEVARS_REPORT_ERROR, 0, &subquery->hasSubLinks); diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index ccdc9bc264ab..25635b6fa8f2 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -206,6 +206,8 @@ static List *set_returning_clause_references(PlannerInfo *root, List *rlist, Plan *topplan, Index resultRelation, + Index exclRelRTI, + indexed_tlist *excl_itlist, int rtoffset); static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, @@ -1086,10 +1088,14 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) { ModifyTable *splan = (ModifyTable *) plan; Plan *subplan = outerPlan(splan); + indexed_tlist *excl_itlist = NULL; Assert(splan->plan.targetlist == NIL); Assert(splan->plan.qual == NIL); + if (splan->onConflictSet) + excl_itlist = build_tlist_index(splan->exclRelTlist); + splan->withCheckOptionLists = fix_scan_list(root, splan->withCheckOptionLists, rtoffset, 1); @@ -1115,6 +1121,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) rlist, subplan, resultrel, + splan->exclRelRTI, + excl_itlist, rtoffset); newRL = lappend(newRL, rlist); } @@ -1142,23 +1150,19 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) */ if (splan->onConflictSet) { - indexed_tlist *itlist; - - itlist = build_tlist_index(splan->exclRelTlist); - splan->onConflictSet = fix_join_expr(root, splan->onConflictSet, - NULL, itlist, + NULL, excl_itlist, linitial_int(splan->resultRelations), rtoffset, NRM_EQUAL, NUM_EXEC_QUAL(plan)); splan->onConflictWhere = (Node *) fix_join_expr(root, (List *) splan->onConflictWhere, - NULL, itlist, + NULL, excl_itlist, linitial_int(splan->resultRelations), rtoffset, NRM_EQUAL, NUM_EXEC_QUAL(plan)); - pfree(itlist); + pfree(excl_itlist); splan->exclRelTlist = fix_scan_list(root, splan->exclRelTlist, rtoffset, 1); @@ -2823,12 +2827,12 @@ build_tlist_index(List *tlist) * build_tlist_index_other_vars --- build a restricted tlist index * * This is like build_tlist_index, but we only index tlist entries that - * are Vars belonging to some rel other than the one specified. We will set + * are Vars belonging to some rel other than the ones specified. We will set * has_ph_vars (allowing PlaceHolderVars to be matched), but not has_non_vars * (so nothing other than Vars and PlaceHolderVars can be matched). */ static indexed_tlist * -build_tlist_index_other_vars(List *tlist, int ignore_rel) +build_tlist_index_other_vars(List *tlist, int ignore_rel1, int ignore_rel2) { indexed_tlist *itlist; tlist_vinfo *vinfo; @@ -2853,7 +2857,7 @@ build_tlist_index_other_vars(List *tlist, int ignore_rel) { Var *var = (Var *) tle->expr; - if (var->varno != ignore_rel) + if (var->varno != ignore_rel1 && var->varno != ignore_rel2) { vinfo->varno = var->varno; vinfo->varattno = var->varattno; @@ -3092,10 +3096,14 @@ search_indexed_tlist_for_sortgroupref(Expr *node, * acceptable_rel should be zero so that any failure to match a Var will be * reported as an error. * 2) RETURNING clauses, which may contain both Vars of the target relation - * and Vars of other relations. In this case we want to replace the - * other-relation Vars by OUTER_VAR references, while leaving target Vars - * alone. Thus inner_itlist = NULL and acceptable_rel = the ID of the - * target relation should be passed. + * and Vars of other relations, including the EXCLUDED pseudo-relation in + * an INSERT ... ON CONFLICT DO UPDATE command. In this case, we want to + * replace references to EXCLUDED with INNER_VAR references, and + * other-relation Vars with OUTER_VAR references, while leaving target Vars + * alone. Thus inner_itlist is to be EXCLUDED elements, if this is an + * INSERT with an ON CONFLICT DO UPDATE clause, outer_itlist is any other + * non-target relation elements, and acceptable_rel = the ID of the target + * relation. * 3) ON CONFLICT UPDATE SET/WHERE clauses. Here references to EXCLUDED are * to be replaced with INNER_VAR references, while leaving target Vars (the * to-be-updated relation) alone. Correspondingly inner_itlist is to be @@ -3156,15 +3164,16 @@ fix_join_expr_mutator(Node *node, fix_join_expr_context *context) /* * Verify that Vars with non-default varreturningtype only appear in - * the RETURNING list, and refer to the target relation. + * the RETURNING list, and that OLD/NEW Vars refer to the target + * relation. */ if (var->varreturningtype != VAR_RETURNING_DEFAULT) { - if (context->inner_itlist != NULL || - context->outer_itlist == NULL || + if (context->outer_itlist == NULL || context->acceptable_rel == 0) - elog(ERROR, "variable returning old/new found outside RETURNING list"); - if (var->varno != context->acceptable_rel) + elog(ERROR, "variable returning old/new/excluded found outside RETURNING list"); + if (var->varreturningtype != VAR_RETURNING_EXCLUDED && + var->varno != context->acceptable_rel) elog(ERROR, "wrong varno %d (expected %d) for variable returning old/new", var->varno, context->acceptable_rel); } @@ -3394,11 +3403,14 @@ fix_upper_expr_mutator(Node *node, fix_upper_expr_context *context) * * If the query involves more than just the result table, we have to * adjust any Vars that refer to other tables to reference junk tlist - * entries in the top subplan's targetlist. Vars referencing the result - * table should be left alone, however (the executor will evaluate them - * using the actual heap tuple, after firing triggers if any). In the - * adjusted RETURNING list, result-table Vars will have their original - * varno (plus rtoffset), but Vars for other rels will have varno OUTER_VAR. + * entries in the top subplan's targetlist. Vars referencing the EXCLUDED + * pseudo-relation of an INSERT ... ON CONFLICT DO UPDATE command should be + * adjusted to reference INNER_VAR, and Vars referencing the result table + * should be left alone (the executor will evaluate them using the actual heap + * tuple, after firing triggers if any). In the adjusted RETURNING list, + * result-table Vars will have their original varno (plus rtoffset), but Vars + * for the EXCLUDED pseudo-relation and other rels will have varno INNER_VAR + * and OUTER_VAR espectively. * * We also must perform opcode lookup and add regclass OIDs to * root->glob->relationOids. @@ -3407,6 +3419,8 @@ fix_upper_expr_mutator(Node *node, fix_upper_expr_context *context) * 'topplan': the top subplan node that will be just below the ModifyTable * node (note it's not yet passed through set_plan_refs) * 'resultRelation': RT index of the associated result relation + * 'exclRelRTI': RT index of EXCLUDED pseudo-relation + * 'excl_itlist': EXCLUDED pseudo-relation elements (or NULL) * 'rtoffset': how much to increment varnos by * * Note: the given 'root' is for the parent query level, not the 'topplan'. @@ -3421,6 +3435,8 @@ set_returning_clause_references(PlannerInfo *root, List *rlist, Plan *topplan, Index resultRelation, + Index exclRelRTI, + indexed_tlist *excl_itlist, int rtoffset) { indexed_tlist *itlist; @@ -3428,9 +3444,11 @@ set_returning_clause_references(PlannerInfo *root, /* * We can perform the desired Var fixup by abusing the fix_join_expr * machinery that formerly handled inner indexscan fixup. We search the - * top plan's targetlist for Vars of non-result relations, and use - * fix_join_expr to convert RETURNING Vars into references to those tlist - * entries, while leaving result-rel Vars as-is. + * top plan's targetlist for Vars of non-result relations (other than + * EXCLUDED), and use fix_join_expr to convert RETURNING Vars into + * references to those tlist entries, and convert RETURNING EXCLUDED Vars + * into references to excl_itlist entries, while leaving result-rel Vars + * as-is. * * PlaceHolderVars will also be sought in the targetlist, but no * more-complex expressions will be. Note that it is not possible for a @@ -3439,12 +3457,13 @@ set_returning_clause_references(PlannerInfo *root, * prepared to pick apart the PlaceHolderVar and evaluate its contained * expression instead. */ - itlist = build_tlist_index_other_vars(topplan->targetlist, resultRelation); + itlist = build_tlist_index_other_vars(topplan->targetlist, resultRelation, + exclRelRTI); rlist = fix_join_expr(root, rlist, itlist, - NULL, + excl_itlist, resultRelation, rtoffset, NRM_EQUAL, diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index 481d8011791b..5c58d0d4d332 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -72,8 +72,7 @@ typedef struct pullup_replace_vars_context PlannerInfo *root; List *targetlist; /* tlist of subquery being pulled up */ RangeTblEntry *target_rte; /* RTE of subquery */ - int result_relation; /* the index of the result relation in the - * rewritten query */ + int new_target_varno; /* see ReplaceVarFromTargetList() */ Relids relids; /* relids within subquery, as numbered after * pullup (set only if target_rte->lateral) */ nullingrel_info *nullinfo; /* per-RTE nullingrel info (set only if @@ -537,11 +536,20 @@ expand_virtual_generated_columns(PlannerInfo *root, Query *parse, * insert into the query, except that we may need to wrap them in * PlaceHolderVars. Set up required context data for * pullup_replace_vars. + * + * In order to handle any Vars with non-default varreturningtype, + * new_target_varno should equal rt_index if it is the result relation + * or the EXCLUDED pseudo-relation. Otherwise, it should be 0. See + * comments in ReplaceVarFromTargetList(). */ rvcontext.root = root; rvcontext.targetlist = tlist; rvcontext.target_rte = rte; - rvcontext.result_relation = parse->resultRelation; + if (rt_index == parse->resultRelation || + (parse->onConflict && rt_index == parse->onConflict->exclRelIndex)) + rvcontext.new_target_varno = rt_index; + else + rvcontext.new_target_varno = 0; /* won't need these values */ rvcontext.relids = NULL; rvcontext.nullinfo = NULL; @@ -1498,7 +1506,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, rvcontext.root = root; rvcontext.targetlist = subquery->targetList; rvcontext.target_rte = rte; - rvcontext.result_relation = 0; + rvcontext.new_target_varno = 0; if (rte->lateral) { rvcontext.relids = get_relids_in_jointree((Node *) subquery->jointree, @@ -2049,7 +2057,7 @@ pull_up_simple_values(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte) rvcontext.root = root; rvcontext.targetlist = tlist; rvcontext.target_rte = rte; - rvcontext.result_relation = 0; + rvcontext.new_target_varno = 0; rvcontext.relids = NULL; /* can't be any lateral references here */ rvcontext.nullinfo = NULL; rvcontext.outer_hasSubLinks = &parse->hasSubLinks; @@ -2209,7 +2217,7 @@ pull_up_constant_function(PlannerInfo *root, Node *jtnode, NULL, /* resname */ false)); /* resjunk */ rvcontext.target_rte = rte; - rvcontext.result_relation = 0; + rvcontext.new_target_varno = 0; /* * Since this function was reduced to a Const, it doesn't contain any @@ -2743,7 +2751,7 @@ pullup_replace_vars_callback(Var *var, newnode = ReplaceVarFromTargetList(var, rcon->target_rte, rcon->targetlist, - rcon->result_relation, + rcon->new_target_varno, REPLACEVARS_REPORT_ERROR, 0); diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c index ffc9d6c3f301..098d8f8498ac 100644 --- a/src/backend/optimizer/prep/preptlist.c +++ b/src/backend/optimizer/prep/preptlist.c @@ -291,7 +291,10 @@ preprocess_targetlist(PlannerInfo *root) * used in RETURNING that belong to other relations. We need to do this * to make these Vars available for the RETURNING calculation. Vars that * belong to the result rel don't need to be added, because they will be - * made to refer to the actual heap tuple. + * made to refer to the actual heap tuple. Vars that refer to the + * EXCLUDED pseudo-relation of an INSERT ... ON CONFLICT DO UPDATE command + * are also not needed, because they are handled specially in the + * executor. */ if (parse->returningList && list_length(parse->rtable) > 1) { @@ -308,7 +311,9 @@ preprocess_targetlist(PlannerInfo *root) TargetEntry *tle; if (IsA(var, Var) && - var->varno == result_relation) + (var->varno == result_relation || + (parse->onConflict && + var->varno == parse->onConflict->exclRelIndex))) continue; /* don't need it */ if (tlist_member((Expr *) var, tlist)) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 81d768ff2a26..5abaff25b0ec 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -3421,7 +3421,7 @@ eval_const_expressions_mutator(Node *node, fselect->resulttypmod, fselect->resultcollid, ((Var *) arg)->varlevelsup); - /* New Var has same OLD/NEW returning as old one */ + /* New Var has same returningtype as old one */ newvar->varreturningtype = ((Var *) arg)->varreturningtype; /* New Var is nullable by same rels as the old one */ newvar->varnullingrels = ((Var *) arg)->varnullingrels; diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c index 8065237a1895..c2642c88e924 100644 --- a/src/backend/optimizer/util/var.c +++ b/src/backend/optimizer/util/var.c @@ -501,8 +501,8 @@ contain_vars_of_level_walker(Node *node, int *sublevels_up) * * Returns true if any found. * - * Any ReturningExprs are also detected --- if an OLD/NEW Var was rewritten, - * we still regard this as a clause that returns OLD/NEW values. + * Any ReturningExprs are also checked --- if an OLD/NEW Var was rewritten, we + * still regard this as a clause that returns OLD/NEW values. * * Does not examine subqueries, therefore must only be used after reduction * of sublinks to subplans! @@ -521,13 +521,16 @@ contain_vars_returning_old_or_new_walker(Node *node, void *context) if (IsA(node, Var)) { if (((Var *) node)->varlevelsup == 0 && - ((Var *) node)->varreturningtype != VAR_RETURNING_DEFAULT) + (((Var *) node)->varreturningtype == VAR_RETURNING_OLD || + ((Var *) node)->varreturningtype == VAR_RETURNING_NEW)) return true; /* abort the tree traversal and return true */ return false; } if (IsA(node, ReturningExpr)) { - if (((ReturningExpr *) node)->retlevelsup == 0) + if (((ReturningExpr *) node)->retlevelsup == 0 && + (((ReturningExpr *) node)->retkind == RETURNING_OLD_EXPR || + ((ReturningExpr *) node)->retkind == RETURNING_NEW_EXPR)) return true; /* abort the tree traversal and return true */ return false; } diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 3b392b084ad6..e82e521dec39 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -1245,12 +1245,21 @@ transformOnConflictClause(ParseState *pstate, EXPR_KIND_WHERE, "WHERE"); /* - * Remove the EXCLUDED pseudo relation from the query namespace, since - * it's not supposed to be available in RETURNING. (Maybe someday we - * could allow that, and drop this step.) + * Leave the EXCLUDED pseudo relation in the query namespace so that + * it is available in RETURNING expressions, but change it to be a + * table-only item so that its columns are only accessible using + * qualified names. This ensures that columns from the target + * relation can be accessed using unqualified names without ambiguity. + * + * Also, set its returning_type so that any RETURNING list Vars + * referencing it are marked correctly. */ Assert((ParseNamespaceItem *) llast(pstate->p_namespace) == exclNSItem); - pstate->p_namespace = list_delete_last(pstate->p_namespace); + exclNSItem->p_cols_visible = false; + + exclNSItem->p_returning_type = VAR_RETURNING_EXCLUDED; + for (int i = 0; i < list_length(exclNSItem->p_names->colnames); i++) + exclNSItem->p_nscolumns[i].p_varreturningtype = VAR_RETURNING_EXCLUDED; } /* Finally, build ON CONFLICT DO [NOTHING | UPDATE] expression */ diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 32d6ae918caa..1e8c24b47185 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -2654,7 +2654,7 @@ transformWholeRowRef(ParseState *pstate, ParseNamespaceItem *nsitem, result = makeWholeRowVar(nsitem->p_rte, nsitem->p_rtindex, sublevels_up, true); - /* mark Var for RETURNING OLD/NEW, as necessary */ + /* mark Var for RETURNING OLD/NEW/EXCLUDED, as necessary */ result->varreturningtype = nsitem->p_returning_type; /* location is not filled in by makeWholeRowVar */ diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 3c80bf1b9ce5..dbd8e26d5197 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -258,7 +258,7 @@ scanNameSpaceForRelid(ParseState *pstate, Oid relid, int location) /* If not inside LATERAL, ignore lateral-only items */ if (nsitem->p_lateral_only && !pstate->p_lateral_active) continue; - /* Ignore OLD/NEW namespace items that can appear in RETURNING */ + /* Ignore OLD/NEW/EXCLUDED namespace items in RETURNING */ if (nsitem->p_returning_type != VAR_RETURNING_DEFAULT) continue; @@ -775,7 +775,7 @@ scanNSItemForColumn(ParseState *pstate, ParseNamespaceItem *nsitem, } var->location = location; - /* Mark Var for RETURNING OLD/NEW, as necessary */ + /* Mark Var for RETURNING OLD/NEW/EXCLUDED, as necessary */ var->varreturningtype = nsitem->p_returning_type; /* Mark Var if it's nulled by any outer joins */ diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index adc9e7600e1e..7e24f2bd20e6 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -97,7 +97,7 @@ static List *matchLocks(CmdType event, Relation relation, static Query *fireRIRrules(Query *parsetree, List *activeRIRs); static Bitmapset *adjust_view_column_set(Bitmapset *cols, List *targetlist); static Node *expand_generated_columns_internal(Node *node, Relation rel, int rt_index, - RangeTblEntry *rte, int result_relation); + RangeTblEntry *rte); /* @@ -643,7 +643,7 @@ rewriteRuleAction(Query *parsetree, 0, rt_fetch(new_varno, sub_action->rtable), parsetree->targetList, - sub_action->resultRelation, + 0, (event == CMD_UPDATE) ? REPLACEVARS_CHANGE_VARNO : REPLACEVARS_SUBSTITUTE_NULL, @@ -2346,7 +2346,7 @@ CopyAndAddInvertedQual(Query *parsetree, rt_fetch(rt_index, parsetree->rtable), parsetree->targetList, - parsetree->resultRelation, + 0, (event == CMD_UPDATE) ? REPLACEVARS_CHANGE_VARNO : REPLACEVARS_SUBSTITUTE_NULL, @@ -3705,12 +3705,12 @@ rewriteTargetView(Query *parsetree, Relation view) BuildOnConflictExcludedTargetlist(base_rel, new_exclRelIndex); /* - * Update all Vars in the ON CONFLICT clause that refer to the old - * EXCLUDED pseudo-relation. We want to use the column mappings - * defined in the view targetlist, but we need the outputs to refer to - * the new EXCLUDED pseudo-relation rather than the new target RTE. - * Also notice that "EXCLUDED.*" will be expanded using the view's - * rowtype, which seems correct. + * Update all Vars in the ON CONFLICT clause and RETURNING list that + * refer to the old EXCLUDED pseudo-relation. We want to use the + * column mappings defined in the view targetlist, but we need the + * outputs to refer to the new EXCLUDED pseudo-relation rather than + * the new target RTE. Also notice that "EXCLUDED.*" will be expanded + * using the view's rowtype, which seems correct. */ tmp_tlist = copyObject(view_targetlist); @@ -3723,7 +3723,18 @@ rewriteTargetView(Query *parsetree, Relation view) 0, view_rte, tmp_tlist, - new_rt_index, + 0, + REPLACEVARS_REPORT_ERROR, + 0, + &parsetree->hasSubLinks); + + parsetree->returningList = (List *) + ReplaceVarsFromTargetList((Node *) parsetree->returningList, + old_exclRelIndex, + 0, + view_rte, + tmp_tlist, + new_exclRelIndex, REPLACEVARS_REPORT_ERROR, 0, &parsetree->hasSubLinks); @@ -4421,13 +4432,11 @@ RewriteQuery(Query *parsetree, List *rewrite_events, int orig_rt_length) * virtual generated column expressions from rel, if there are any. * * The caller must also provide rte, the RTE describing the target relation, - * in order to handle any whole-row Vars referencing the target, and - * result_relation, the index of the result relation, if this is part of an - * INSERT/UPDATE/DELETE/MERGE query. + * in order to handle any whole-row Vars referencing the target. */ static Node * expand_generated_columns_internal(Node *node, Relation rel, int rt_index, - RangeTblEntry *rte, int result_relation) + RangeTblEntry *rte) { TupleDesc tupdesc; @@ -4455,8 +4464,7 @@ expand_generated_columns_internal(Node *node, Relation rel, int rt_index, Assert(list_length(tlist) > 0); - node = ReplaceVarsFromTargetList(node, rt_index, 0, rte, tlist, - result_relation, + node = ReplaceVarsFromTargetList(node, rt_index, 0, rte, tlist, 0, REPLACEVARS_CHANGE_VARNO, rt_index, NULL); } @@ -4485,7 +4493,7 @@ expand_generated_columns_in_expr(Node *node, Relation rel, int rt_index) rte->rtekind = RTE_RELATION; rte->relid = RelationGetRelid(rel); - node = expand_generated_columns_internal(node, rel, rt_index, rte, 0); + node = expand_generated_columns_internal(node, rel, rt_index, rte); } return node; diff --git a/src/backend/rewrite/rewriteManip.c b/src/backend/rewrite/rewriteManip.c index cd786aa4112b..e5f85116f79a 100644 --- a/src/backend/rewrite/rewriteManip.c +++ b/src/backend/rewrite/rewriteManip.c @@ -921,7 +921,7 @@ IncrementVarSublevelsUp_rtable(List *rtable, int delta_sublevels_up, /* * SetVarReturningType - adjust Var nodes for a specified varreturningtype. * - * Find all Var nodes referring to the specified result relation in the given + * Find all Var nodes referring to the specified 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 @@ -931,7 +931,7 @@ IncrementVarSublevelsUp_rtable(List *rtable, int delta_sublevels_up, typedef struct { - int result_relation; + int target_varno; int sublevels_up; VarReturningType returning_type; } SetVarReturningType_context; @@ -945,7 +945,7 @@ SetVarReturningType_walker(Node *node, SetVarReturningType_context *context) { Var *var = (Var *) node; - if (var->varno == context->result_relation && + if (var->varno == context->target_varno && var->varlevelsup == context->sublevels_up) var->varreturningtype = context->returning_type; @@ -967,12 +967,12 @@ SetVarReturningType_walker(Node *node, SetVarReturningType_context *context) } static void -SetVarReturningType(Node *node, int result_relation, int sublevels_up, +SetVarReturningType(Node *node, int target_varno, int sublevels_up, VarReturningType returning_type) { SetVarReturningType_context context; - context.result_relation = result_relation; + context.target_varno = target_varno; context.sublevels_up = sublevels_up; context.returning_type = returning_type; @@ -1744,14 +1744,29 @@ map_variable_attnos(Node *node, * relation. This is needed to handle whole-row Vars referencing the target. * We expand such Vars into RowExpr constructs. * - * In addition, for INSERT/UPDATE/DELETE/MERGE queries, the caller must - * provide result_relation, the index of the result relation in the rewritten - * query. This is needed to handle OLD/NEW RETURNING list Vars referencing - * target_varno. When such Vars are expanded, their varreturningtype is - * copied onto any replacement Vars referencing result_relation. In addition, - * if the replacement expression from the targetlist is not simply a Var - * referencing result_relation, it is wrapped in a ReturningExpr node (causing - * the executor to return NULL if the OLD/NEW row doesn't exist). + * In addition, the caller should provide new_target_varno, which is needed to + * handle any OLD/NEW/EXCLUDED RETURNING list Vars (Vars with non-default + * varreturningtype) referencing target_varno. When such Vars are expanded, + * their varreturningtype is copied onto any Vars that reference + * new_target_varno in the replacement expression from the targetlist. In + * addition, if the replacement expression is not simply a Var referencing + * new_target_varno, it is wrapped in a ReturningExpr node (causing the + * executor to return NULL if the OLD/NEW/EXCLUDED row doesn't exist). The + * caller should set new_target_varno as follows: + * + * If the input node contains Vars from the RETURNING list of a query, and + * target_varno is the resultRelation of that query, then new_target_varno + * should be the (possibly new) resultRelation of the rewritten query. + * + * If the input node contains Vars from the RETURNING list of an INSERT ... + * ON CONFLICT DO UPDATE query, and target_varno is the index of the + * EXCLUDED pseudo-relation, then new_target_varno should be the (possibly + * new) index of the EXCLUDED pseudo-relation in the rewritten query. + * + * Otherwise, new_target_varno should be set to 0 in order to detect any + * Vars with non-default varreturningtype outside the RETURNING list, or + * referencing a relation other than the result relation or the EXCLUDED + * pseudo-relation. * * Note that ReplaceVarFromTargetList always generates the replacement * expression with varlevelsup = 0. The caller is responsible for adjusting @@ -1765,7 +1780,7 @@ typedef struct { RangeTblEntry *target_rte; List *targetlist; - int result_relation; + int new_target_varno; ReplaceVarsNoMatchOption nomatch_option; int nomatch_varno; } ReplaceVarsFromTargetList_context; @@ -1780,7 +1795,7 @@ ReplaceVarsFromTargetList_callback(Var *var, newnode = ReplaceVarFromTargetList(var, rcon->target_rte, rcon->targetlist, - rcon->result_relation, + rcon->new_target_varno, rcon->nomatch_option, rcon->nomatch_varno); @@ -1795,7 +1810,7 @@ Node * ReplaceVarFromTargetList(Var *var, RangeTblEntry *target_rte, List *targetlist, - int result_relation, + int new_target_varno, ReplaceVarsNoMatchOption nomatch_option, int nomatch_varno) { @@ -1844,7 +1859,7 @@ ReplaceVarFromTargetList(Var *var, field = ReplaceVarFromTargetList((Var *) field, target_rte, targetlist, - result_relation, + new_target_varno, nomatch_option, nomatch_varno); rowexpr->args = lappend(rowexpr->args, field); @@ -1856,7 +1871,7 @@ ReplaceVarFromTargetList(Var *var, ReturningExpr *rexpr = makeNode(ReturningExpr); rexpr->retlevelsup = 0; - rexpr->retold = (var->varreturningtype == VAR_RETURNING_OLD); + rexpr->retkind = (ReturningExprKind) var->varreturningtype; rexpr->retexpr = (Expr *) rowexpr; return (Node *) rexpr; @@ -1925,28 +1940,28 @@ ReplaceVarFromTargetList(Var *var, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("NEW variables in ON UPDATE rules cannot reference columns that are part of a multiple assignment in the subject UPDATE command"))); - /* Handle any OLD/NEW RETURNING list Vars */ + /* Handle any OLD/NEW/EXCLUDED RETURNING list Vars */ if (var->varreturningtype != VAR_RETURNING_DEFAULT) { /* * Copy varreturningtype onto any Vars in the tlist item that - * refer to result_relation (which had better be non-zero). + * refer to new_target_varno (which had better be non-zero). */ - if (result_relation == 0) - elog(ERROR, "variable returning old/new found outside RETURNING list"); + if (new_target_varno == 0) + elog(ERROR, "variable returning old/new/excluded found outside RETURNING list, or referencing a relation other than the result relation or the EXCLUDED pseudo-relation"); - SetVarReturningType((Node *) newnode, result_relation, + SetVarReturningType((Node *) newnode, new_target_varno, 0, var->varreturningtype); /* Wrap it in a ReturningExpr, if needed, per comments above */ if (!IsA(newnode, Var) || - ((Var *) newnode)->varno != result_relation || + ((Var *) newnode)->varno != new_target_varno || ((Var *) newnode)->varlevelsup != 0) { ReturningExpr *rexpr = makeNode(ReturningExpr); rexpr->retlevelsup = 0; - rexpr->retold = (var->varreturningtype == VAR_RETURNING_OLD); + rexpr->retkind = (ReturningExprKind) var->varreturningtype; rexpr->retexpr = newnode; newnode = (Expr *) rexpr; @@ -1962,7 +1977,7 @@ ReplaceVarsFromTargetList(Node *node, int target_varno, int sublevels_up, RangeTblEntry *target_rte, List *targetlist, - int result_relation, + int new_target_varno, ReplaceVarsNoMatchOption nomatch_option, int nomatch_varno, bool *outer_hasSubLinks) @@ -1971,7 +1986,7 @@ ReplaceVarsFromTargetList(Node *node, context.target_rte = target_rte; context.targetlist = targetlist; - context.result_relation = result_relation; + context.new_target_varno = new_target_varno; context.nomatch_option = nomatch_option; context.nomatch_varno = nomatch_varno; diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 75366203706c..1ce4d98afced 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -26,9 +26,9 @@ struct JsonConstructorExprState; /* Bits in ExprState->flags (see also execnodes.h for public flag bits): */ /* expression's interpreter has been initialized */ -#define EEO_FLAG_INTERPRETER_INITIALIZED (1 << 5) +#define EEO_FLAG_INTERPRETER_INITIALIZED (1 << 6) /* jump-threading is in use */ -#define EEO_FLAG_DIRECT_THREADED (1 << 6) +#define EEO_FLAG_DIRECT_THREADED (1 << 7) /* Typical API for out-of-line evaluation subroutines */ typedef void (*ExecEvalSubroutine) (ExprState *state, @@ -338,7 +338,8 @@ typedef struct ExprEvalStep /* but it's just the normal (negative) attr number for SYSVAR */ int attnum; Oid vartype; /* type OID of variable */ - VarReturningType varreturningtype; /* return old/new/default */ + /* if not default, return old/new/excluded value */ + VarReturningType varreturningtype; } var; /* for EEOP_WHOLEROW */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 18ae8f0d4bb8..00ffff03d691 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -80,6 +80,8 @@ typedef Datum (*ExprStateEvalFunc) (ExprState *expression, #define EEO_FLAG_OLD_IS_NULL (1 << 3) /* NEW table row is NULL in RETURNING list */ #define EEO_FLAG_NEW_IS_NULL (1 << 4) +/* INNER (EXCLUDED) table row is NULL in RETURNING list */ +#define EEO_FLAG_INNER_IS_NULL (1 << 5) typedef struct ExprState { diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 1b4436f2ff6d..06c79dfcf839 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -227,7 +227,9 @@ typedef struct Expr * varreturningtype is used for Vars that refer to the target relation in the * RETURNING list of data-modifying queries. The default behavior is to * return old values for DELETE and new values for INSERT and UPDATE, but it - * is also possible to explicitly request old or new values. + * is also possible to explicitly request old or new values. For INSERT ... + * ON CONFLICT DO UPDATE, varreturningtype is also used for Vars in the + * RETURNING list that refer to the EXCLUDED pseudo-relation. * * In the parser, varnosyn and varattnosyn are either identical to * varno/varattno, or they specify the column's position in an aliased JOIN @@ -256,6 +258,7 @@ 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 */ + VAR_RETURNING_EXCLUDED, /* return EXCLUDED on conflict, else NULL */ } VarReturningType; typedef struct Var @@ -2154,14 +2157,15 @@ typedef struct InferenceElem } InferenceElem; /* - * ReturningExpr - return OLD/NEW.(expression) in RETURNING list + * ReturningExpr - return OLD/NEW/EXCLUDED.(expression) in RETURNING list * * This is used when updating an auto-updatable view and returning a view * column that is not simply a Var referring to the base relation. In such - * cases, OLD/NEW.viewcol can expand to an arbitrary expression, but the - * result is required to be NULL if the OLD/NEW row doesn't exist. To handle - * this, the rewriter wraps the expanded expression in a ReturningExpr, which - * is equivalent to "CASE WHEN (OLD/NEW row exists) THEN (expr) ELSE NULL". + * cases, OLD/NEW/EXCLUDED.viewcol can expand to an arbitrary expression, but + * the result is required to be NULL if the OLD/NEW/EXCLUDED row doesn't + * exist. To handle this, the rewriter wraps the expanded expression in a + * ReturningExpr, which is equivalent to "CASE WHEN (OLD/NEW/EXCLUDED row + * exists) THEN (expr) ELSE NULL". * * A similar situation can arise when rewriting the RETURNING clause of a * rule, which may also contain arbitrary expressions. @@ -2169,11 +2173,19 @@ typedef struct InferenceElem * ReturningExpr nodes never appear in a parsed Query --- they are only ever * inserted by the rewriter and the planner. */ +typedef enum ReturningExprKind +{ + /* values here match non-default VarReturningType values */ + RETURNING_OLD_EXPR = VAR_RETURNING_OLD, + RETURNING_NEW_EXPR = VAR_RETURNING_NEW, + RETURNING_EXCLUDED_EXPR = VAR_RETURNING_EXCLUDED, +} ReturningExprKind; + typedef struct ReturningExpr { Expr xpr; int retlevelsup; /* > 0 if it belongs to outer query */ - bool retold; /* true for OLD, false for NEW */ + ReturningExprKind retkind; /* return OLD/NEW/EXCLUDED expression */ Expr *retexpr; /* expression to be returned */ } ReturningExpr; diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index f7d07c845425..1f0c21543b2d 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -280,7 +280,8 @@ struct ParseState * 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 + * refer to the OLD and NEW state of the result relation, and the EXCLUDED + * pseudo-relation for an INSERT ... ON CONFLICT DO UPDATE. These namespace * items have p_returning_type set appropriately, for use when creating Vars. * For convenience, this information is duplicated on each namespace column. * @@ -301,7 +302,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? */ + VarReturningType p_returning_type; /* for RETURNING OLD/NEW/EXCLUDED */ }; /* @@ -332,7 +333,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 */ + VarReturningType p_varreturningtype; /* for RETURNING OLD/NEW/EXCLUDED */ 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/rewrite/rewriteManip.h b/src/include/rewrite/rewriteManip.h index 74de195deeb2..1c7e4ae855fe 100644 --- a/src/include/rewrite/rewriteManip.h +++ b/src/include/rewrite/rewriteManip.h @@ -107,14 +107,14 @@ extern Node *map_variable_attnos(Node *node, extern Node *ReplaceVarFromTargetList(Var *var, RangeTblEntry *target_rte, List *targetlist, - int result_relation, + int new_target_varno, ReplaceVarsNoMatchOption nomatch_option, int nomatch_varno); extern Node *ReplaceVarsFromTargetList(Node *node, int target_varno, int sublevels_up, RangeTblEntry *target_rte, List *targetlist, - int result_relation, + int new_target_varno, ReplaceVarsNoMatchOption nomatch_option, int nomatch_varno, bool *outer_hasSubLinks); diff --git a/src/test/regress/expected/arrays.out b/src/test/regress/expected/arrays.out index 69ea2cf5ad80..fc3c150b6b49 100644 --- a/src/test/regress/expected/arrays.out +++ b/src/test/regress/expected/arrays.out @@ -1398,20 +1398,30 @@ create temp table arr_pk_tbl (pk int4 primary key, f1 int[]); insert into arr_pk_tbl values (1, '{1,2,3}'); insert into arr_pk_tbl values (1, '{3,4,5}') on conflict (pk) do update set f1[1] = excluded.f1[1], f1[3] = excluded.f1[3] - returning pk, f1; - pk | f1 -----+--------- - 1 | {3,2,5} + returning pk, f1, excluded.f1 as "excluded f1"; + pk | f1 | excluded f1 +----+---------+------------- + 1 | {3,2,5} | {3,4,5} (1 row) insert into arr_pk_tbl(pk, f1[1:2]) values (1, '{6,7,8}') on conflict (pk) do update set f1[1] = excluded.f1[1], f1[2] = excluded.f1[2], f1[3] = excluded.f1[3] - returning pk, f1; - pk | f1 -----+------------ - 1 | {6,7,NULL} + returning pk, f1, excluded.f1 as "excluded f1"; + pk | f1 | excluded f1 +----+------------+------------- + 1 | {6,7,NULL} | {6,7} +(1 row) + +insert into arr_pk_tbl(pk, f1[2]) values (1, 10) on conflict (pk) + do update set f1[1] = excluded.f1[1], + f1[2] = excluded.f1[2], + f1[3] = excluded.f1[3] + returning pk, f1, excluded.f1 as "excluded f1"; + pk | f1 | excluded f1 +----+----------------+------------- + 1 | {NULL,10,NULL} | [2:2]={10} (1 row) -- note: if above selects don't produce the expected tuple order, diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out index b3710a49de62..fedb49acd5f8 100644 --- a/src/test/regress/expected/generated_stored.out +++ b/src/test/regress/expected/generated_stored.out @@ -114,32 +114,44 @@ INSERT INTO gtest1 VALUES (3, 33), (4, DEFAULT); -- error ERROR: cannot insert a non-DEFAULT value into column "b" DETAIL: Column "b" is a generated column. INSERT INTO gtest1 VALUES (3, DEFAULT), (4, DEFAULT); -- ok +INSERT INTO gtest1 VALUES (4, DEFAULT), (5, DEFAULT) + ON CONFLICT (a) DO UPDATE SET a = excluded.a + 100 + RETURNING old.*, new.*, excluded.*; + a | b | a | b | a | b +---+---+-----+-----+---+--- + 4 | 8 | 104 | 208 | 4 | 8 + | | 5 | 10 | | +(2 rows) + SELECT * FROM gtest1 ORDER BY a; - a | b ----+--- - 1 | 2 - 2 | 4 - 3 | 6 - 4 | 8 -(4 rows) + a | b +-----+----- + 1 | 2 + 2 | 4 + 3 | 6 + 5 | 10 + 104 | 208 +(5 rows) SELECT gtest1 FROM gtest1 ORDER BY a; -- whole-row reference - gtest1 --------- + gtest1 +----------- (1,2) (2,4) (3,6) - (4,8) -(4 rows) + (5,10) + (104,208) +(5 rows) SELECT a, (SELECT gtest1.b) FROM gtest1 ORDER BY a; -- sublink - a | b ----+--- - 1 | 2 - 2 | 4 - 3 | 6 - 4 | 8 -(4 rows) + a | b +-----+----- + 1 | 2 + 2 | 4 + 3 | 6 + 5 | 10 + 104 | 208 +(5 rows) DELETE FROM gtest1 WHERE a >= 3; UPDATE gtest1 SET b = DEFAULT WHERE a = 1; diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out index 4ec3d3300175..a25dc80303e9 100644 --- a/src/test/regress/expected/generated_virtual.out +++ b/src/test/regress/expected/generated_virtual.out @@ -114,32 +114,44 @@ INSERT INTO gtest1 VALUES (3, 33), (4, DEFAULT); -- error ERROR: cannot insert a non-DEFAULT value into column "b" DETAIL: Column "b" is a generated column. INSERT INTO gtest1 VALUES (3, DEFAULT), (4, DEFAULT); -- ok +INSERT INTO gtest1 VALUES (4, DEFAULT), (5, DEFAULT) + ON CONFLICT (a) DO UPDATE SET a = excluded.a + 100 + RETURNING old.*, new.*, excluded.*; + a | b | a | b | a | b +---+---+-----+-----+---+--- + 4 | 8 | 104 | 208 | 4 | 8 + | | 5 | 10 | | +(2 rows) + SELECT * FROM gtest1 ORDER BY a; - a | b ----+--- - 1 | 2 - 2 | 4 - 3 | 6 - 4 | 8 -(4 rows) + a | b +-----+----- + 1 | 2 + 2 | 4 + 3 | 6 + 5 | 10 + 104 | 208 +(5 rows) SELECT gtest1 FROM gtest1 ORDER BY a; -- whole-row reference - gtest1 --------- + gtest1 +----------- (1,2) (2,4) (3,6) - (4,8) -(4 rows) + (5,10) + (104,208) +(5 rows) SELECT a, (SELECT gtest1.b) FROM gtest1 ORDER BY a; -- sublink - a | b ----+--- - 1 | 2 - 2 | 4 - 3 | 6 - 4 | 8 -(4 rows) + a | b +-----+----- + 1 | 2 + 2 | 4 + 3 | 6 + 5 | 10 + 104 | 208 +(5 rows) DELETE FROM gtest1 WHERE a >= 3; UPDATE gtest1 SET b = DEFAULT WHERE a = 1; diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out index 0490a746555c..6c57548661ff 100644 --- a/src/test/regress/expected/inherit.out +++ b/src/test/regress/expected/inherit.out @@ -2131,7 +2131,14 @@ select * from inhpar; -- Also check ON CONFLICT insert into inhpar as i values (3), (7) on conflict (f1) - do update set (f1, f2) = (select i.f1, i.f2 || '+'); + do update set (f1, f2) = (select i.f1, i.f2 || '+') + returning old, new, excluded; + old | new | excluded +--------+---------+---------- + (3,3-) | (3,3-+) | (3,) + (7,7-) | (7,7-+) | (7,) +(2 rows) + select * from inhpar order by f1; -- tuple order might be unstable here f1 | f2 ----+----- diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out index db6684746842..28cc6332f1e2 100644 --- a/src/test/regress/expected/insert_conflict.out +++ b/src/test/regress/expected/insert_conflict.out @@ -249,13 +249,13 @@ insert into insertconflicttest values (2, 'Orange') on conflict (key, key, key) insert into insertconflicttest values (1, 'Apple'), (2, 'Orange') on conflict (key) do update set (fruit, key) = (excluded.fruit, excluded.key); --- Give good diagnostic message when EXCLUDED.* spuriously referenced from --- RETURNING: +-- EXCLUDED.* referenced from RETURNING: insert into insertconflicttest values (1, 'Apple') on conflict (key) do update set fruit = excluded.fruit RETURNING excluded.fruit; -ERROR: invalid reference to FROM-clause entry for table "excluded" -LINE 1: ...y) do update set fruit = excluded.fruit RETURNING excluded.f... - ^ -DETAIL: There is an entry for table "excluded", but it cannot be referenced from this part of the query. + fruit +------- + Apple +(1 row) + -- Only suggest .* column when inference element misspelled: insert into insertconflicttest values (1, 'Apple') on conflict (keyy) do update set fruit = excluded.fruit; ERROR: column "keyy" does not exist @@ -408,33 +408,33 @@ drop index partial_key_index; create unique index plain on insertconflicttest(key); -- Succeeds, updates existing row: insert into insertconflicttest as i values (23, 'Jackfruit') on conflict (key) do update set fruit = excluded.fruit - where i.* != excluded.* returning *; - key | fruit ------+----------- - 23 | Jackfruit + where i.* != excluded.* returning *, excluded.* = old.*, excluded.* = new.*; + key | fruit | ?column? | ?column? +-----+-----------+----------+---------- + 23 | Jackfruit | f | t (1 row) -- No update this time, though: insert into insertconflicttest as i values (23, 'Jackfruit') on conflict (key) do update set fruit = excluded.fruit - where i.* != excluded.* returning *; - key | fruit ------+------- + where i.* != excluded.* returning *, excluded.* = old.*, excluded.* = new.*; + key | fruit | ?column? | ?column? +-----+-------+----------+---------- (0 rows) -- Predicate changed to require match rather than non-match, so updates once more: insert into insertconflicttest as i values (23, 'Jackfruit') on conflict (key) do update set fruit = excluded.fruit - where i.* = excluded.* returning *; - key | fruit ------+----------- - 23 | Jackfruit + where i.* = excluded.* returning *, excluded.* = old.*, excluded.* = new.*; + key | fruit | ?column? | ?column? +-----+-----------+----------+---------- + 23 | Jackfruit | t | t (1 row) -- Assign: insert into insertconflicttest as i values (23, 'Avocado') on conflict (key) do update set fruit = excluded.*::text - returning *; - key | fruit ------+-------------- - 23 | (23,Avocado) + returning *, excluded.* = old.*, excluded.* = new.*; + key | fruit | ?column? | ?column? +-----+--------------+----------+---------- + 23 | (23,Avocado) | f | f (1 row) -- deparse whole row var in WHERE and SET clauses: @@ -472,6 +472,10 @@ insert into syscolconflicttest values (1) on conflict (key) do update set data = ERROR: column excluded.ctid does not exist LINE 1: ...values (1) on conflict (key) do update set data = excluded.c... ^ +insert into syscolconflicttest values (1) on conflict (key) do update set data = excluded.data returning excluded.ctid; +ERROR: column excluded.ctid does not exist +LINE 1: ...key) do update set data = excluded.data returning excluded.c... + ^ drop table syscolconflicttest; -- -- Previous tests all managed to not test any expressions requiring @@ -546,7 +550,13 @@ select * from capitals; -- Succeeds: insert into cities values ('Las Vegas', 2.583E+5, 2174) on conflict do nothing; -insert into capitals values ('Sacramento', 4664.E+5, 30, 'CA') on conflict (name) do update set population = excluded.population; +insert into capitals values ('Sacramento', 4664.E+5, 30, 'CA') on conflict (name) do update set population = excluded.population + returning old.*, new.*, excluded.*; + name | population | altitude | state | name | population | altitude | state | name | population | altitude | state +------------+------------+----------+-------+------------+------------+----------+-------+------------+------------+----------+------- + Sacramento | 369400 | 30 | CA | Sacramento | 466400000 | 30 | CA | Sacramento | 466400000 | 30 | CA +(1 row) + -- Wrong "Sacramento", so do nothing: insert into capitals values ('Sacramento', 50, 2267, 'NE') on conflict (name) do nothing; select * from capitals; @@ -556,7 +566,13 @@ select * from capitals; Sacramento | 466400000 | 30 | CA (2 rows) -insert into cities values ('Las Vegas', 5.83E+5, 2001) on conflict (name) do update set population = excluded.population, altitude = excluded.altitude; +insert into cities values ('Las Vegas', 5.83E+5, 2001) on conflict (name) do update set population = excluded.population, altitude = excluded.altitude + returning old.*, new.*, excluded.*; + name | population | altitude | name | population | altitude | name | population | altitude +-----------+------------+----------+-----------+------------+----------+-----------+------------+---------- + Las Vegas | 258300 | 2174 | Las Vegas | 583000 | 2001 | Las Vegas | 583000 | 2001 +(1 row) + select tableoid::regclass, * from cities; tableoid | name | population | altitude ----------+---------------+------------+---------- @@ -567,7 +583,13 @@ select tableoid::regclass, * from cities; capitals | Sacramento | 466400000 | 30 (5 rows) -insert into capitals values ('Las Vegas', 5.83E+5, 2222, 'NV') on conflict (name) do update set population = excluded.population; +insert into capitals values ('Las Vegas', 5.83E+5, 2222, 'NV') on conflict (name) do update set population = excluded.population + returning old.*, new.*, excluded.*; + name | population | altitude | state | name | population | altitude | state | name | population | altitude | state +------+------------+----------+-------+-----------+------------+----------+-------+------+------------+----------+------- + | | | | Las Vegas | 583000 | 2222 | NV | | | | +(1 row) + -- Capitals will contain new capital, Las Vegas: select * from capitals; name | population | altitude | state @@ -591,7 +613,13 @@ select tableoid::regclass, * from cities; (6 rows) -- This only affects "cities" version of "Las Vegas": -insert into cities values ('Las Vegas', 5.86E+5, 2223) on conflict (name) do update set population = excluded.population, altitude = excluded.altitude; +insert into cities values ('Las Vegas', 5.86E+5, 2223) on conflict (name) do update set population = excluded.population, altitude = excluded.altitude + returning old.*, new.*, excluded.*; + name | population | altitude | name | population | altitude | name | population | altitude +-----------+------------+----------+-----------+------------+----------+-----------+------------+---------- + Las Vegas | 583000 | 2001 | Las Vegas | 586000 | 2223 | Las Vegas | 586000 | 2223 +(1 row) + select tableoid::regclass, * from cities; tableoid | name | population | altitude ----------+---------------+------------+---------- @@ -628,11 +656,17 @@ insert into excluded AS target values(1, '2') on conflict (key) do update set da 1 | 2 (1 row) --- make sure excluded isn't a problem in returning clause +-- error, ambiguous insert into excluded values(1, '2') on conflict (key) do update set data = 3 RETURNING excluded.*; - key | data ------+------ - 1 | 3 +ERROR: table reference "excluded" is ambiguous +LINE 1: ...n conflict (key) do update set data = 3 RETURNING excluded.*... + ^ +-- ok, aliased +insert into excluded AS target values(1, '2') on conflict (key) do update set data = 3 +RETURNING target.*, excluded.*; + key | data | key | data +-----+------+-----+------ + 1 | 3 | 1 | 2 (1 row) -- clean up diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out index d02c2ceab53a..ec6e4d53d3a4 100644 --- a/src/test/regress/expected/returning.out +++ b/src/test/regress/expected/returning.out @@ -461,18 +461,19 @@ INSERT INTO foo VALUES (4) | | | | | | foo | (0,4) | 4 | | 42 | 99 | 4 | | 42 | 99 (1 row) --- INSERT ... ON CONFLICT ... UPDATE has OLD and NEW +-- INSERT ... ON CONFLICT ... UPDATE has OLD and NEW (and EXLCUDED) CREATE UNIQUE INDEX foo_f1_idx ON foo (f1); EXPLAIN (verbose, costs off) 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.*, *; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------------------------------------------- + n.tableoid::regclass, n.ctid, n.*, *, + excluded.*; + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Insert on pg_temp.foo - Output: (o.tableoid)::regclass, o.ctid, o.f1, o.f2, o.f3, o.f4, (n.tableoid)::regclass, n.ctid, n.f1, n.f2, n.f3, n.f4, foo.f1, foo.f2, foo.f3, foo.f4 + Output: (o.tableoid)::regclass, o.ctid, o.f1, o.f2, o.f3, o.f4, (n.tableoid)::regclass, n.ctid, n.f1, n.f2, n.f3, n.f4, foo.f1, foo.f2, foo.f3, foo.f4, excluded.f1, excluded.f2, excluded.f3, excluded.f4 Conflict Resolution: UPDATE Conflict Arbiter Indexes: foo_f1_idx -> Values Scan on "*VALUES*" @@ -483,11 +484,12 @@ 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 | (0,6) | 5 | ok | 42 | 99 | 5 | ok | 42 | 99 + n.tableoid::regclass, n.ctid, n.*, *, + excluded.*; + tableoid | ctid | f1 | f2 | f3 | f4 | tableoid | ctid | f1 | f2 | f3 | f4 | 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 | 4 | conflict | 42 | 99 + | | | | | | foo | (0,6) | 5 | ok | 42 | 99 | 5 | ok | 42 | 99 | | | | (2 rows) -- UPDATE has OLD and NEW diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 2bf968ae3d37..044e3d778bdf 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -3560,6 +3560,67 @@ SELECT * FROM hat_data WHERE hat_name IN ('h8', 'h9', 'h7') ORDER BY hat_name; h9 | blue (3 rows) +DROP RULE hat_upsert ON hats; +-- DO UPDATE ... RETURNING excluded values +CREATE RULE hat_upsert AS ON INSERT TO hats + DO INSTEAD + INSERT INTO hat_data VALUES ( + NEW.hat_name, + NEW.hat_color) + ON CONFLICT (hat_name) + DO UPDATE + SET hat_name = hat_data.hat_name, hat_color = 'Excl: '||excluded.hat_color + WHERE excluded.hat_color <> 'forbidden' AND hat_data.* != excluded.* + RETURNING excluded.*; +SELECT definition FROM pg_rules WHERE tablename = 'hats' ORDER BY rulename; + definition +--------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE RULE hat_upsert AS + + ON INSERT TO public.hats DO INSTEAD INSERT INTO hat_data (hat_name, hat_color) + + VALUES (new.hat_name, new.hat_color) ON CONFLICT(hat_name) DO UPDATE SET hat_name = hat_data.hat_name, hat_color = ('Excl: '::text || (excluded.hat_color)::text)+ + WHERE ((excluded.hat_color <> 'forbidden'::bpchar) AND (hat_data.* <> excluded.*)) + + RETURNING excluded.hat_name, + + excluded.hat_color; +(1 row) + +-- EXCLUDED not allowed in plain INSERT +INSERT INTO hats VALUES ('h6', 'black'), ('h7', 'forbidden'), ('h8', 'red') + RETURNING excluded.*; +ERROR: missing FROM-clause entry for table "excluded" +LINE 2: RETURNING excluded.*; + ^ +-- OLD/NEW have no effect on EXCLUDED +EXPLAIN (verbose, costs off) +INSERT INTO hats VALUES ('h6', 'black'), ('h7', 'forbidden'), ('h8', 'red') + RETURNING old.*, new.*, *; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------- + Insert on public.hat_data + Output: excluded.hat_name, excluded.hat_color, excluded.hat_name, excluded.hat_color, excluded.hat_name, excluded.hat_color + Conflict Resolution: UPDATE + Conflict Arbiter Indexes: hat_data_unique_idx + Conflict Filter: ((excluded.hat_color <> 'forbidden'::bpchar) AND (hat_data.* <> excluded.*)) + -> Values Scan on "*VALUES*" + Output: "*VALUES*".column1, "*VALUES*".column2 +(7 rows) + +INSERT INTO hats VALUES ('h6', 'black'), ('h7', 'forbidden'), ('h8', 'red') + RETURNING old.*, new.*, *; + hat_name | hat_color | hat_name | hat_color | hat_name | hat_color +------------+------------+------------+------------+------------+------------ + | | | | | + h8 | red | h8 | red | h8 | red +(2 rows) + +SELECT * FROM hat_data ORDER BY hat_name; + hat_name | hat_color +------------+------------ + h6 | black + h7 | black + h8 | Excl: red + h9 | blue +(4 rows) + DROP RULE hat_upsert ON hats; drop table hats; drop table hat_data; diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 1eb8fba09537..26538808a53a 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -1708,51 +1708,103 @@ end; $$; create trigger upsert_after_trig after insert or update on upsert for each row execute procedure upsert_after_func(); -insert into upsert values(1, 'black') on conflict (key) do update set color = 'updated ' || upsert.color; +insert into upsert values(1, 'black') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; WARNING: before insert (new): (1,black) WARNING: after insert (new): (1,black) -insert into upsert values(2, 'red') on conflict (key) do update set color = 'updated ' || upsert.color; + key | color | key | color | key | color +-----+-------+-----+-------+-----+------- + | | 1 | black | | +(1 row) + +insert into upsert values(2, 'red') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; WARNING: before insert (new): (2,red) WARNING: before insert (new, modified): (3,"red trig modified") WARNING: after insert (new): (3,"red trig modified") -insert into upsert values(3, 'orange') on conflict (key) do update set color = 'updated ' || upsert.color; + key | color | key | color | key | color +-----+-------+-----+-------------------+-----+------- + | | 3 | red trig modified | | +(1 row) + +insert into upsert values(3, 'orange') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; WARNING: before insert (new): (3,orange) WARNING: before update (old): (3,"red trig modified") WARNING: before update (new): (3,"updated red trig modified") WARNING: after update (old): (3,"red trig modified") WARNING: after update (new): (3,"updated red trig modified") -insert into upsert values(4, 'green') on conflict (key) do update set color = 'updated ' || upsert.color; + key | color | key | color | key | color +-----+-------------------+-----+---------------------------+-----+-------- + 3 | red trig modified | 3 | updated red trig modified | 3 | orange +(1 row) + +insert into upsert values(4, 'green') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; WARNING: before insert (new): (4,green) WARNING: before insert (new, modified): (5,"green trig modified") WARNING: after insert (new): (5,"green trig modified") -insert into upsert values(5, 'purple') on conflict (key) do update set color = 'updated ' || upsert.color; + key | color | key | color | key | color +-----+-------+-----+---------------------+-----+------- + | | 5 | green trig modified | | +(1 row) + +insert into upsert values(5, 'purple') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; WARNING: before insert (new): (5,purple) WARNING: before update (old): (5,"green trig modified") WARNING: before update (new): (5,"updated green trig modified") WARNING: after update (old): (5,"green trig modified") WARNING: after update (new): (5,"updated green trig modified") -insert into upsert values(6, 'white') on conflict (key) do update set color = 'updated ' || upsert.color; + key | color | key | color | key | color +-----+---------------------+-----+-----------------------------+-----+-------- + 5 | green trig modified | 5 | updated green trig modified | 5 | purple +(1 row) + +insert into upsert values(6, 'white') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; WARNING: before insert (new): (6,white) WARNING: before insert (new, modified): (7,"white trig modified") WARNING: after insert (new): (7,"white trig modified") -insert into upsert values(7, 'pink') on conflict (key) do update set color = 'updated ' || upsert.color; + key | color | key | color | key | color +-----+-------+-----+---------------------+-----+------- + | | 7 | white trig modified | | +(1 row) + +insert into upsert values(7, 'pink') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; WARNING: before insert (new): (7,pink) WARNING: before update (old): (7,"white trig modified") WARNING: before update (new): (7,"updated white trig modified") WARNING: after update (old): (7,"white trig modified") WARNING: after update (new): (7,"updated white trig modified") -insert into upsert values(8, 'yellow') on conflict (key) do update set color = 'updated ' || upsert.color; + key | color | key | color | key | color +-----+---------------------+-----+-----------------------------+-----+------- + 7 | white trig modified | 7 | updated white trig modified | 7 | pink +(1 row) + +insert into upsert values(8, 'yellow') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; WARNING: before insert (new): (8,yellow) WARNING: before insert (new, modified): (9,"yellow trig modified") WARNING: after insert (new): (9,"yellow trig modified") + key | color | key | color | key | color +-----+-------+-----+----------------------+-----+------- + | | 9 | yellow trig modified | | +(1 row) + +insert into upsert values(8, 'gold') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +WARNING: before insert (new): (8,gold) +WARNING: before insert (new, modified): (9,"gold trig modified") +WARNING: before update (old): (9,"yellow trig modified") +WARNING: before update (new): (9,"updated yellow trig modified") +WARNING: after update (old): (9,"yellow trig modified") +WARNING: after update (new): (9,"updated yellow trig modified") + key | color | key | color | key | color +-----+----------------------+-----+------------------------------+-----+-------------------- + 9 | yellow trig modified | 9 | updated yellow trig modified | 9 | gold trig modified +(1 row) + select * from upsert; - key | color ------+----------------------------- + key | color +-----+------------------------------ 1 | black 3 | updated red trig modified 5 | updated green trig modified 7 | updated white trig modified - 9 | yellow trig modified + 9 | updated yellow trig modified (5 rows) drop table upsert; diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index 03df7e75b7b6..37b79a5f00f6 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -3653,7 +3653,13 @@ insert into uv_iocu_tab values ('xyxyxy', 0); create view uv_iocu_view as select b, b+1 as c, a, '2.0'::text as two from uv_iocu_tab; insert into uv_iocu_view (a, b) values ('xyxyxy', 1) - on conflict (a) do update set b = uv_iocu_view.b; + on conflict (a) do update set b = uv_iocu_view.b + returning old.*, new.*, excluded.*; + b | c | a | two | b | c | a | two | b | c | a | two +---+---+--------+-----+---+---+--------+-----+---+---+--------+----- + 0 | 1 | xyxyxy | 2.0 | 0 | 1 | xyxyxy | 2.0 | 1 | 2 | xyxyxy | 2.0 +(1 row) + select * from uv_iocu_tab; a | b --------+--- @@ -3661,7 +3667,13 @@ select * from uv_iocu_tab; (1 row) insert into uv_iocu_view (a, b) values ('xyxyxy', 1) - on conflict (a) do update set b = excluded.b; + on conflict (a) do update set b = excluded.b + returning old.*, new.*, excluded.*; + b | c | a | two | b | c | a | two | b | c | a | two +---+---+--------+-----+---+---+--------+-----+---+---+--------+----- + 0 | 1 | xyxyxy | 2.0 | 1 | 2 | xyxyxy | 2.0 | 1 | 2 | xyxyxy | 2.0 +(1 row) + select * from uv_iocu_tab; a | b --------+--- @@ -3710,7 +3722,8 @@ insert into uv_iocu_view (aa,bb) values (1,'y') on conflict (aa) do update set bb = 'Rejected: '||excluded.* where excluded.aa > 0 and excluded.bb != '' - and excluded.cc is not null; + and excluded.cc is not null + returning 'Old: '||old.*, 'New: '||new.*, 'Excluded: '||excluded.*; QUERY PLAN --------------------------------------------------------------------------------------------------------- Insert on uv_iocu_tab @@ -3724,7 +3737,13 @@ insert into uv_iocu_view (aa,bb) values (1,'y') on conflict (aa) do update set bb = 'Rejected: '||excluded.* where excluded.aa > 0 and excluded.bb != '' - and excluded.cc is not null; + and excluded.cc is not null + returning 'Old: '||old.*, 'New: '||new.*, 'Excluded: '||excluded.*; + ?column? | ?column? | ?column? +--------------------+------------------------------------------------------------------------------+------------------------- + Old: (x,1,"(1,x)") | New: ("Rejected: (y,1,""(1,y)"")",1,"(1,""Rejected: (y,1,""""(1,y)"""")"")") | Excluded: (y,1,"(1,y)") +(1 row) + select * from uv_iocu_view; bb | aa | cc -------------------------+----+--------------------------------- diff --git a/src/test/regress/sql/arrays.sql b/src/test/regress/sql/arrays.sql index 47d62c1d38d2..47a5d7c9d72c 100644 --- a/src/test/regress/sql/arrays.sql +++ b/src/test/regress/sql/arrays.sql @@ -426,12 +426,17 @@ create temp table arr_pk_tbl (pk int4 primary key, f1 int[]); insert into arr_pk_tbl values (1, '{1,2,3}'); insert into arr_pk_tbl values (1, '{3,4,5}') on conflict (pk) do update set f1[1] = excluded.f1[1], f1[3] = excluded.f1[3] - returning pk, f1; + returning pk, f1, excluded.f1 as "excluded f1"; insert into arr_pk_tbl(pk, f1[1:2]) values (1, '{6,7,8}') on conflict (pk) do update set f1[1] = excluded.f1[1], f1[2] = excluded.f1[2], f1[3] = excluded.f1[3] - returning pk, f1; + returning pk, f1, excluded.f1 as "excluded f1"; +insert into arr_pk_tbl(pk, f1[2]) values (1, 10) on conflict (pk) + do update set f1[1] = excluded.f1[1], + f1[2] = excluded.f1[2], + f1[3] = excluded.f1[3] + returning pk, f1, excluded.f1 as "excluded f1"; -- note: if above selects don't produce the expected tuple order, -- then you didn't get an indexscan plan, and something is busted. diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql index 99ea0105685c..6a3e114e35c7 100644 --- a/src/test/regress/sql/generated_stored.sql +++ b/src/test/regress/sql/generated_stored.sql @@ -58,6 +58,10 @@ INSERT INTO gtest1 VALUES (3, DEFAULT), (4, 44); -- error INSERT INTO gtest1 VALUES (3, 33), (4, DEFAULT); -- error INSERT INTO gtest1 VALUES (3, DEFAULT), (4, DEFAULT); -- ok +INSERT INTO gtest1 VALUES (4, DEFAULT), (5, DEFAULT) + ON CONFLICT (a) DO UPDATE SET a = excluded.a + 100 + RETURNING old.*, new.*, excluded.*; + SELECT * FROM gtest1 ORDER BY a; SELECT gtest1 FROM gtest1 ORDER BY a; -- whole-row reference SELECT a, (SELECT gtest1.b) FROM gtest1 ORDER BY a; -- sublink diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql index 992c0cdae65e..994f0ef27ad6 100644 --- a/src/test/regress/sql/generated_virtual.sql +++ b/src/test/regress/sql/generated_virtual.sql @@ -58,6 +58,10 @@ INSERT INTO gtest1 VALUES (3, DEFAULT), (4, 44); -- error INSERT INTO gtest1 VALUES (3, 33), (4, DEFAULT); -- error INSERT INTO gtest1 VALUES (3, DEFAULT), (4, DEFAULT); -- ok +INSERT INTO gtest1 VALUES (4, DEFAULT), (5, DEFAULT) + ON CONFLICT (a) DO UPDATE SET a = excluded.a + 100 + RETURNING old.*, new.*, excluded.*; + SELECT * FROM gtest1 ORDER BY a; SELECT gtest1 FROM gtest1 ORDER BY a; -- whole-row reference SELECT a, (SELECT gtest1.b) FROM gtest1 ORDER BY a; -- sublink diff --git a/src/test/regress/sql/inherit.sql b/src/test/regress/sql/inherit.sql index 699e8ac09c88..da1d418abfc3 100644 --- a/src/test/regress/sql/inherit.sql +++ b/src/test/regress/sql/inherit.sql @@ -806,7 +806,8 @@ select * from inhpar; -- Also check ON CONFLICT insert into inhpar as i values (3), (7) on conflict (f1) - do update set (f1, f2) = (select i.f1, i.f2 || '+'); + do update set (f1, f2) = (select i.f1, i.f2 || '+') + returning old, new, excluded; select * from inhpar order by f1; -- tuple order might be unstable here drop table inhpar cascade; diff --git a/src/test/regress/sql/insert_conflict.sql b/src/test/regress/sql/insert_conflict.sql index 549c46452ec0..738dea09e448 100644 --- a/src/test/regress/sql/insert_conflict.sql +++ b/src/test/regress/sql/insert_conflict.sql @@ -101,8 +101,7 @@ insert into insertconflicttest values (1, 'Apple'), (2, 'Orange') on conflict (key) do update set (fruit, key) = (excluded.fruit, excluded.key); --- Give good diagnostic message when EXCLUDED.* spuriously referenced from --- RETURNING: +-- EXCLUDED.* referenced from RETURNING: insert into insertconflicttest values (1, 'Apple') on conflict (key) do update set fruit = excluded.fruit RETURNING excluded.fruit; -- Only suggest
.* column when inference element misspelled: @@ -238,16 +237,16 @@ create unique index plain on insertconflicttest(key); -- Succeeds, updates existing row: insert into insertconflicttest as i values (23, 'Jackfruit') on conflict (key) do update set fruit = excluded.fruit - where i.* != excluded.* returning *; + where i.* != excluded.* returning *, excluded.* = old.*, excluded.* = new.*; -- No update this time, though: insert into insertconflicttest as i values (23, 'Jackfruit') on conflict (key) do update set fruit = excluded.fruit - where i.* != excluded.* returning *; + where i.* != excluded.* returning *, excluded.* = old.*, excluded.* = new.*; -- Predicate changed to require match rather than non-match, so updates once more: insert into insertconflicttest as i values (23, 'Jackfruit') on conflict (key) do update set fruit = excluded.fruit - where i.* = excluded.* returning *; + where i.* = excluded.* returning *, excluded.* = old.*, excluded.* = new.*; -- Assign: insert into insertconflicttest as i values (23, 'Avocado') on conflict (key) do update set fruit = excluded.*::text - returning *; + returning *, excluded.* = old.*, excluded.* = new.*; -- deparse whole row var in WHERE and SET clauses: explain (costs off) insert into insertconflicttest as i values (23, 'Avocado') on conflict (key) do update set fruit = excluded.fruit where excluded.* is null; explain (costs off) insert into insertconflicttest as i values (23, 'Avocado') on conflict (key) do update set fruit = excluded.*::text; @@ -267,6 +266,7 @@ drop table insertconflicttest; create table syscolconflicttest(key int4, data text); insert into syscolconflicttest values (1); insert into syscolconflicttest values (1) on conflict (key) do update set data = excluded.ctid::text; +insert into syscolconflicttest values (1) on conflict (key) do update set data = excluded.data returning excluded.ctid; drop table syscolconflicttest; -- @@ -344,20 +344,24 @@ select * from capitals; -- Succeeds: insert into cities values ('Las Vegas', 2.583E+5, 2174) on conflict do nothing; -insert into capitals values ('Sacramento', 4664.E+5, 30, 'CA') on conflict (name) do update set population = excluded.population; +insert into capitals values ('Sacramento', 4664.E+5, 30, 'CA') on conflict (name) do update set population = excluded.population + returning old.*, new.*, excluded.*; -- Wrong "Sacramento", so do nothing: insert into capitals values ('Sacramento', 50, 2267, 'NE') on conflict (name) do nothing; select * from capitals; -insert into cities values ('Las Vegas', 5.83E+5, 2001) on conflict (name) do update set population = excluded.population, altitude = excluded.altitude; +insert into cities values ('Las Vegas', 5.83E+5, 2001) on conflict (name) do update set population = excluded.population, altitude = excluded.altitude + returning old.*, new.*, excluded.*; select tableoid::regclass, * from cities; -insert into capitals values ('Las Vegas', 5.83E+5, 2222, 'NV') on conflict (name) do update set population = excluded.population; +insert into capitals values ('Las Vegas', 5.83E+5, 2222, 'NV') on conflict (name) do update set population = excluded.population + returning old.*, new.*, excluded.*; -- Capitals will contain new capital, Las Vegas: select * from capitals; -- Cities contains two instances of "Las Vegas", since unique constraints don't -- work across inheritance: select tableoid::regclass, * from cities; -- This only affects "cities" version of "Las Vegas": -insert into cities values ('Las Vegas', 5.86E+5, 2223) on conflict (name) do update set population = excluded.population, altitude = excluded.altitude; +insert into cities values ('Las Vegas', 5.86E+5, 2223) on conflict (name) do update set population = excluded.population, altitude = excluded.altitude + returning old.*, new.*, excluded.*; select tableoid::regclass, * from cities; -- clean up @@ -374,8 +378,11 @@ insert into excluded values(1, '2') on conflict (key) do update set data = exclu insert into excluded AS target values(1, '2') on conflict (key) do update set data = excluded.data RETURNING *; -- ok, aliased insert into excluded AS target values(1, '2') on conflict (key) do update set data = target.data RETURNING *; --- make sure excluded isn't a problem in returning clause +-- error, ambiguous insert into excluded values(1, '2') on conflict (key) do update set data = 3 RETURNING excluded.*; +-- ok, aliased +insert into excluded AS target values(1, '2') on conflict (key) do update set data = 3 +RETURNING target.*, excluded.*; -- clean up drop table excluded; diff --git a/src/test/regress/sql/returning.sql b/src/test/regress/sql/returning.sql index cc99cb53f63c..00fa8a78c3d3 100644 --- a/src/test/regress/sql/returning.sql +++ b/src/test/regress/sql/returning.sql @@ -209,19 +209,21 @@ 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 +-- INSERT ... ON CONFLICT ... UPDATE has OLD and NEW (and EXLCUDED) CREATE UNIQUE INDEX foo_f1_idx ON foo (f1); EXPLAIN (verbose, costs off) 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.*, *; + n.tableoid::regclass, n.ctid, n.*, *, + excluded.*; 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.*, *; + n.tableoid::regclass, n.ctid, n.*, *, + excluded.*; -- UPDATE has OLD and NEW EXPLAIN (verbose, costs off) diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql index 3f240bec7b0a..3dc7dffa9f62 100644 --- a/src/test/regress/sql/rules.sql +++ b/src/test/regress/sql/rules.sql @@ -1205,6 +1205,33 @@ SELECT * FROM hat_data WHERE hat_name IN ('h8', 'h9', 'h7') ORDER BY hat_name; DROP RULE hat_upsert ON hats; +-- DO UPDATE ... RETURNING excluded values +CREATE RULE hat_upsert AS ON INSERT TO hats + DO INSTEAD + INSERT INTO hat_data VALUES ( + NEW.hat_name, + NEW.hat_color) + ON CONFLICT (hat_name) + DO UPDATE + SET hat_name = hat_data.hat_name, hat_color = 'Excl: '||excluded.hat_color + WHERE excluded.hat_color <> 'forbidden' AND hat_data.* != excluded.* + RETURNING excluded.*; +SELECT definition FROM pg_rules WHERE tablename = 'hats' ORDER BY rulename; + +-- EXCLUDED not allowed in plain INSERT +INSERT INTO hats VALUES ('h6', 'black'), ('h7', 'forbidden'), ('h8', 'red') + RETURNING excluded.*; + +-- OLD/NEW have no effect on EXCLUDED +EXPLAIN (verbose, costs off) +INSERT INTO hats VALUES ('h6', 'black'), ('h7', 'forbidden'), ('h8', 'red') + RETURNING old.*, new.*, *; +INSERT INTO hats VALUES ('h6', 'black'), ('h7', 'forbidden'), ('h8', 'red') + RETURNING old.*, new.*, *; +SELECT * FROM hat_data ORDER BY hat_name; + +DROP RULE hat_upsert ON hats; + drop table hats; drop table hat_data; diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 5f7f75d7ba5d..5790de6b662c 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1189,14 +1189,15 @@ $$; create trigger upsert_after_trig after insert or update on upsert for each row execute procedure upsert_after_func(); -insert into upsert values(1, 'black') on conflict (key) do update set color = 'updated ' || upsert.color; -insert into upsert values(2, 'red') on conflict (key) do update set color = 'updated ' || upsert.color; -insert into upsert values(3, 'orange') on conflict (key) do update set color = 'updated ' || upsert.color; -insert into upsert values(4, 'green') on conflict (key) do update set color = 'updated ' || upsert.color; -insert into upsert values(5, 'purple') on conflict (key) do update set color = 'updated ' || upsert.color; -insert into upsert values(6, 'white') on conflict (key) do update set color = 'updated ' || upsert.color; -insert into upsert values(7, 'pink') on conflict (key) do update set color = 'updated ' || upsert.color; -insert into upsert values(8, 'yellow') on conflict (key) do update set color = 'updated ' || upsert.color; +insert into upsert values(1, 'black') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +insert into upsert values(2, 'red') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +insert into upsert values(3, 'orange') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +insert into upsert values(4, 'green') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +insert into upsert values(5, 'purple') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +insert into upsert values(6, 'white') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +insert into upsert values(7, 'pink') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +insert into upsert values(8, 'yellow') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; +insert into upsert values(8, 'gold') on conflict (key) do update set color = 'updated ' || upsert.color returning old.*, new.*, excluded.*; select * from upsert; diff --git a/src/test/regress/sql/updatable_views.sql b/src/test/regress/sql/updatable_views.sql index c071fffc1163..91f45fa4509a 100644 --- a/src/test/regress/sql/updatable_views.sql +++ b/src/test/regress/sql/updatable_views.sql @@ -1858,10 +1858,12 @@ create view uv_iocu_view as select b, b+1 as c, a, '2.0'::text as two from uv_iocu_tab; insert into uv_iocu_view (a, b) values ('xyxyxy', 1) - on conflict (a) do update set b = uv_iocu_view.b; + on conflict (a) do update set b = uv_iocu_view.b + returning old.*, new.*, excluded.*; select * from uv_iocu_tab; insert into uv_iocu_view (a, b) values ('xyxyxy', 1) - on conflict (a) do update set b = excluded.b; + on conflict (a) do update set b = excluded.b + returning old.*, new.*, excluded.*; select * from uv_iocu_tab; -- OK to access view columns that are not present in underlying base @@ -1892,12 +1894,14 @@ insert into uv_iocu_view (aa,bb) values (1,'y') on conflict (aa) do update set bb = 'Rejected: '||excluded.* where excluded.aa > 0 and excluded.bb != '' - and excluded.cc is not null; + and excluded.cc is not null + returning 'Old: '||old.*, 'New: '||new.*, 'Excluded: '||excluded.*; insert into uv_iocu_view (aa,bb) values (1,'y') on conflict (aa) do update set bb = 'Rejected: '||excluded.* where excluded.aa > 0 and excluded.bb != '' - and excluded.cc is not null; + and excluded.cc is not null + returning 'Old: '||old.*, 'New: '||new.*, 'Excluded: '||excluded.*; select * from uv_iocu_view; -- Test omitting a column of the base relation diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 018b5919cf66..712952deefad 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2582,6 +2582,7 @@ ReturnSetInfo ReturnStmt ReturningClause ReturningExpr +ReturningExprKind ReturningOption ReturningOptionKind RevmapContents