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