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 3f1399177905..d1c056296a13 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 1f2da072632e..94b49f445c82 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 6cc6966b0600..017a685959b1 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -4013,8 +4013,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 d706546f3326..df8f9b4baa8e 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,
@@ -1065,10 +1067,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);
@@ -1094,6 +1100,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
rlist,
subplan,
resultrel,
+ splan->exclRelRTI,
+ excl_itlist,
rtoffset);
newRL = lappend(newRL, rlist);
}
@@ -1121,23 +1129,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);
@@ -2802,12 +2806,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;
@@ -2832,7 +2836,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;
@@ -3071,10 +3075,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
@@ -3135,15 +3143,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);
}
@@ -3373,11 +3382,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.
@@ -3386,6 +3398,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'.
@@ -3400,6 +3414,8 @@ set_returning_clause_references(PlannerInfo *root,
List *rlist,
Plan *topplan,
Index resultRelation,
+ Index exclRelRTI,
+ indexed_tlist *excl_itlist,
int rtoffset)
{
indexed_tlist *itlist;
@@ -3407,9 +3423,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
@@ -3418,12 +3436,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 35e8d3c183b4..b859b30ba14f 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;
@@ -1496,7 +1504,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,
@@ -2047,7 +2055,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;
@@ -2207,7 +2215,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
@@ -2741,7 +2749,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 ae0bd073ca91..70a44db88aab 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -3418,7 +3418,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 b9763ea17144..5d9e72adafa5 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1234,12 +1234,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 e1979a80c198..5be6029f0124 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -2652,7 +2652,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 04ecf64b1fc2..1651c52eb879 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 71857feae482..15c58e869708 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -82,6 +82,8 @@ typedef Datum (*ExprStateEvalFunc) (struct 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 6dfca3cb35ba..a867814cd8fc 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
@@ -2140,14 +2143,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.
@@ -2155,11 +2159,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 7c018f2a4e35..81a1493ad31f 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 b815473f414b..5a5a06e6a06d 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 adac2cedfb2a..5eae1029550d 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 aca6347babe9..2d15dcee07e2 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 5b5055babdcb..d32cd36bba00 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -2125,7 +2125,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 fdd0f6c8f258..58017bb0eb65 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 341b689f7665..4ad70c171b50 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 35e8aad7701b..d554b86a1fce 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3537,6 +3537,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 095df0a670c2..e1bdcfb80bcd 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 f56fde8d4e5d..de3ceb7e8ade 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 ba19bc4c701e..f5cba5dd8f35 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 fdd3ff1d161c..ed60cecbe7fc 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 a13e81628902..42b29c200060 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2574,6 +2574,7 @@ ReturnSetInfo
ReturnStmt
ReturningClause
ReturningExpr
+ReturningExprKind
ReturningOption
ReturningOptionKind
RevmapContents