1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

Add support for INSERT ... ON CONFLICT DO SELECT.

This adds a new ON CONFLICT action DO SELECT [FOR UPDATE/SHARE], which
returns the pre-existing rows when conflicts are detected. The INSERT
statement must have a RETURNING clause, when DO SELECT is specified.

The optional FOR UPDATE/SHARE clause allows the rows to be locked
before they are are returned. As with a DO UPDATE conflict action, an
optional WHERE clause may be used to prevent rows from being selected
for return (but as with a DO UPDATE action, rows filtered out by the
WHERE clause are still locked).

Bumps catversion as stored rules change.

Author: Andreas Karlsson <andreas@proxel.se>
Author: Marko Tiikkaja <marko@joh.to>
Author: Viktor Holmberg <v@viktorh.net>
Reviewed-by: Joel Jacobson <joel@compiler.org>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Reviewed-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Jian He <jian.universality@gmail.com>
Discussion: https://postgr.es/m/d631b406-13b7-433e-8c0b-c6040c4b4663@Spark
Discussion: https://postgr.es/m/5fca222d-62ae-4a2f-9fcb-0eca56277094@Spark
Discussion: https://postgr.es/m/2b5db2e6-8ece-44d0-9890-f256fdca9f7e@proxel.se
Discussion: https://postgr.es/m/CAL9smLCdV-v3KgOJX3mU19FYK82N7yzqJj2HAwWX70E=P98kgQ@mail.gmail.com
This commit is contained in:
Dean Rasheed
2026-02-12 09:55:06 +00:00
parent 788ec96d59
commit 88327092ff
48 changed files with 1672 additions and 349 deletions

View File

@@ -147,12 +147,24 @@ static void ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
ItemPointer tupleid,
TupleTableSlot *oldslot,
TupleTableSlot *newslot);
static bool ExecOnConflictLockRow(ModifyTableContext *context,
TupleTableSlot *existing,
ItemPointer conflictTid,
Relation relation,
LockTupleMode lockmode,
bool isUpdate);
static bool ExecOnConflictUpdate(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer conflictTid,
TupleTableSlot *excludedSlot,
bool canSetTag,
TupleTableSlot **returning);
static bool ExecOnConflictSelect(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer conflictTid,
TupleTableSlot *excludedSlot,
bool canSetTag,
TupleTableSlot **returning);
static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
EState *estate,
PartitionTupleRouting *proute,
@@ -274,7 +286,7 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
*
* context: context for the ModifyTable operation
* resultRelInfo: current result rel
* cmdType: operation/merge action performed (INSERT, UPDATE, or DELETE)
* isDelete: true if the operation/merge action is a DELETE
* oldSlot: slot holding old tuple deleted or updated
* newSlot: slot holding new tuple inserted or updated
* planSlot: slot holding tuple returned by top subplan node
@@ -283,12 +295,15 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
* econtext's scan tuple and its old & new tuples are not needed (FDW direct-
* modify is disabled if the RETURNING list refers to any OLD/NEW values).
*
* Note: For the SELECT path of INSERT ... ON CONFLICT DO SELECT, oldSlot and
* newSlot are both the existing tuple, since it's not changed.
*
* Returns a slot holding the result tuple
*/
static TupleTableSlot *
ExecProcessReturning(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
CmdType cmdType,
bool isDelete,
TupleTableSlot *oldSlot,
TupleTableSlot *newSlot,
TupleTableSlot *planSlot)
@@ -298,23 +313,17 @@ ExecProcessReturning(ModifyTableContext *context,
ExprContext *econtext = projectReturning->pi_exprContext;
/* Make tuple and any needed join variables available to ExecProject */
switch (cmdType)
if (isDelete)
{
case CMD_INSERT:
case CMD_UPDATE:
/* return new tuple by default */
if (newSlot)
econtext->ecxt_scantuple = newSlot;
break;
case CMD_DELETE:
/* return old tuple by default */
if (oldSlot)
econtext->ecxt_scantuple = oldSlot;
break;
default:
elog(ERROR, "unrecognized commandType: %d", (int) cmdType);
/* return old tuple by default */
if (oldSlot)
econtext->ecxt_scantuple = oldSlot;
}
else
{
/* return new tuple by default */
if (newSlot)
econtext->ecxt_scantuple = newSlot;
}
econtext->ecxt_outertuple = planSlot;
@@ -1158,6 +1167,26 @@ ExecInsert(ModifyTableContext *context,
else
goto vlock;
}
else if (onconflict == ONCONFLICT_SELECT)
{
/*
* In case of ON CONFLICT DO SELECT, optionally lock the
* conflicting tuple, fetch it and project RETURNING on
* it. Be prepared to retry if locking fails because of a
* concurrent UPDATE/DELETE to the conflict tuple.
*/
TupleTableSlot *returning = NULL;
if (ExecOnConflictSelect(context, resultRelInfo,
&conflictTid, slot, canSetTag,
&returning))
{
InstrCountTuples2(&mtstate->ps, 1);
return returning;
}
else
goto vlock;
}
else
{
/*
@@ -1329,7 +1358,7 @@ ExecInsert(ModifyTableContext *context,
}
}
result = ExecProcessReturning(context, resultRelInfo, CMD_INSERT,
result = ExecProcessReturning(context, resultRelInfo, false,
oldSlot, slot, planSlot);
/*
@@ -1890,7 +1919,7 @@ ldelete:
return NULL;
}
rslot = ExecProcessReturning(context, resultRelInfo, CMD_DELETE,
rslot = ExecProcessReturning(context, resultRelInfo, true,
slot, NULL, context->planSlot);
/*
@@ -2692,56 +2721,37 @@ redo_act:
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
return ExecProcessReturning(context, resultRelInfo, CMD_UPDATE,
return ExecProcessReturning(context, resultRelInfo, false,
oldSlot, slot, context->planSlot);
return NULL;
}
/*
* ExecOnConflictUpdate --- execute UPDATE of INSERT ON CONFLICT DO UPDATE
* ExecOnConflictLockRow --- lock the row for ON CONFLICT DO SELECT/UPDATE
*
* Try to lock tuple for update as part of speculative insertion. If
* a qual originating from ON CONFLICT DO UPDATE is satisfied, update
* (but still lock row, even though it may not satisfy estate's
* snapshot).
* Try to lock tuple for update as part of speculative insertion for ON
* CONFLICT DO UPDATE or ON CONFLICT DO SELECT FOR UPDATE/SHARE.
*
* Returns true if we're done (with or without an update), or false if
* the caller must retry the INSERT from scratch.
* Returns true if the row is successfully locked, or false if the caller must
* retry the INSERT from scratch.
*/
static bool
ExecOnConflictUpdate(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer conflictTid,
TupleTableSlot *excludedSlot,
bool canSetTag,
TupleTableSlot **returning)
ExecOnConflictLockRow(ModifyTableContext *context,
TupleTableSlot *existing,
ItemPointer conflictTid,
Relation relation,
LockTupleMode lockmode,
bool isUpdate)
{
ModifyTableState *mtstate = context->mtstate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
Relation relation = resultRelInfo->ri_RelationDesc;
ExprState *onConflictSetWhere = resultRelInfo->ri_onConflict->oc_WhereClause;
TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing;
TM_FailureData tmfd;
LockTupleMode lockmode;
TM_Result test;
Datum xminDatum;
TransactionId xmin;
bool isnull;
/*
* Parse analysis should have blocked ON CONFLICT for all system
* relations, which includes these. There's no fundamental obstacle to
* supporting this; we'd just need to handle LOCKTAG_TUPLE like the other
* ExecUpdate() caller.
*/
Assert(!resultRelInfo->ri_needLockTagTuple);
/* Determine lock mode to use */
lockmode = ExecUpdateLockMode(context->estate, resultRelInfo);
/*
* Lock tuple for update. Don't follow updates when tuple cannot be
* Lock tuple with lockmode. Don't follow updates when tuple cannot be
* locked without doing so. A row locking conflict here means our
* previous conclusion that the tuple is conclusively committed is not
* true anymore.
@@ -2786,7 +2796,7 @@ ExecOnConflictUpdate(ModifyTableContext *context,
(errcode(ERRCODE_CARDINALITY_VIOLATION),
/* translator: %s is a SQL command name */
errmsg("%s command cannot affect row a second time",
"ON CONFLICT DO UPDATE"),
isUpdate ? "ON CONFLICT DO UPDATE" : "ON CONFLICT DO SELECT"),
errhint("Ensure that no rows proposed for insertion within the same command have duplicate constrained values.")));
/* This shouldn't happen */
@@ -2834,6 +2844,50 @@ ExecOnConflictUpdate(ModifyTableContext *context,
}
/* Success, the tuple is locked. */
return true;
}
/*
* ExecOnConflictUpdate --- execute UPDATE of INSERT ON CONFLICT DO UPDATE
*
* Try to lock tuple for update as part of speculative insertion. If
* a qual originating from ON CONFLICT DO UPDATE is satisfied, update
* (but still lock row, even though it may not satisfy estate's
* snapshot).
*
* Returns true if we're done (with or without an update), or false if
* the caller must retry the INSERT from scratch.
*/
static bool
ExecOnConflictUpdate(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer conflictTid,
TupleTableSlot *excludedSlot,
bool canSetTag,
TupleTableSlot **returning)
{
ModifyTableState *mtstate = context->mtstate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
Relation relation = resultRelInfo->ri_RelationDesc;
ExprState *onConflictSetWhere = resultRelInfo->ri_onConflict->oc_WhereClause;
TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing;
LockTupleMode lockmode;
/*
* Parse analysis should have blocked ON CONFLICT for all system
* relations, which includes these. There's no fundamental obstacle to
* supporting this; we'd just need to handle LOCKTAG_TUPLE like the other
* ExecUpdate() caller.
*/
Assert(!resultRelInfo->ri_needLockTagTuple);
/* Determine lock mode to use */
lockmode = ExecUpdateLockMode(context->estate, resultRelInfo);
/* Lock tuple for update */
if (!ExecOnConflictLockRow(context, existing, conflictTid,
resultRelInfo->ri_RelationDesc, lockmode, true))
return false;
/*
* Verify that the tuple is visible to our MVCC snapshot if the current
@@ -2875,11 +2929,13 @@ ExecOnConflictUpdate(ModifyTableContext *context,
* security barrier quals (if any), enforced here as RLS checks/WCOs.
*
* The rewriter creates UPDATE RLS checks/WCOs for UPDATE security
* quals, and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK,
* but that's almost the extent of its special handling for ON
* CONFLICT DO UPDATE.
* quals, and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK.
* Since SELECT permission on the target table is always required for
* INSERT ... ON CONFLICT DO UPDATE, the rewriter also adds SELECT RLS
* checks/WCOs for SELECT security quals, using WCOs of the same kind,
* and this check enforces them too.
*
* The rewriter will also have associated UPDATE applicable straight
* The rewriter will also have associated UPDATE-applicable straight
* RLS checks/WCOs for the benefit of the ExecUpdate() call that
* follows. INSERTs and UPDATEs naturally have mutually exclusive WCO
* kinds, so there is no danger of spurious over-enforcement in the
@@ -2924,6 +2980,141 @@ ExecOnConflictUpdate(ModifyTableContext *context,
return true;
}
/*
* ExecOnConflictSelect --- execute SELECT of INSERT ON CONFLICT DO SELECT
*
* If SELECT FOR UPDATE/SHARE is specified, try to lock tuple as part of
* speculative insertion. If a qual originating from ON CONFLICT DO SELECT is
* satisfied, select (but still lock row, even though it may not satisfy
* estate's snapshot).
*
* Returns true if we're done (with or without a select), or false if the
* caller must retry the INSERT from scratch.
*/
static bool
ExecOnConflictSelect(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer conflictTid,
TupleTableSlot *excludedSlot,
bool canSetTag,
TupleTableSlot **returning)
{
ModifyTableState *mtstate = context->mtstate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
Relation relation = resultRelInfo->ri_RelationDesc;
ExprState *onConflictSelectWhere = resultRelInfo->ri_onConflict->oc_WhereClause;
TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing;
LockClauseStrength lockStrength = resultRelInfo->ri_onConflict->oc_LockStrength;
/*
* Parse analysis should have blocked ON CONFLICT for all system
* relations, which includes these. There's no fundamental obstacle to
* supporting this; we'd just need to handle LOCKTAG_TUPLE appropriately.
*/
Assert(!resultRelInfo->ri_needLockTagTuple);
/* Fetch/lock existing tuple, according to the requested lock strength */
if (lockStrength == LCS_NONE)
{
if (!table_tuple_fetch_row_version(relation,
conflictTid,
SnapshotAny,
existing))
elog(ERROR, "failed to fetch conflicting tuple for ON CONFLICT");
}
else
{
LockTupleMode lockmode;
switch (lockStrength)
{
case LCS_FORKEYSHARE:
lockmode = LockTupleKeyShare;
break;
case LCS_FORSHARE:
lockmode = LockTupleShare;
break;
case LCS_FORNOKEYUPDATE:
lockmode = LockTupleNoKeyExclusive;
break;
case LCS_FORUPDATE:
lockmode = LockTupleExclusive;
break;
default:
elog(ERROR, "Unexpected lock strength %d", (int) lockStrength);
}
if (!ExecOnConflictLockRow(context, existing, conflictTid,
resultRelInfo->ri_RelationDesc, lockmode, false))
return false;
}
/*
* Verify that the tuple is visible to our MVCC snapshot if the current
* isolation level mandates that. See comments in ExecOnConflictUpdate().
*/
ExecCheckTupleVisible(context->estate, relation, existing);
/*
* Make tuple and any needed join variables available to ExecQual. The
* EXCLUDED tuple is installed in ecxt_innertuple, while the target's
* existing tuple is installed in the scantuple. EXCLUDED has been made
* to reference INNER_VAR in setrefs.c, but there is no other redirection.
*/
econtext->ecxt_scantuple = existing;
econtext->ecxt_innertuple = excludedSlot;
econtext->ecxt_outertuple = NULL;
if (!ExecQual(onConflictSelectWhere, econtext))
{
ExecClearTuple(existing); /* see return below */
InstrCountFiltered1(&mtstate->ps, 1);
return true; /* done with the tuple */
}
if (resultRelInfo->ri_WithCheckOptions != NIL)
{
/*
* Check target's existing tuple against SELECT-applicable USING
* security barrier quals (if any), enforced here as RLS checks/WCOs.
*
* The rewriter creates WCOs from the USING quals of SELECT policies,
* and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK. If FOR
* UPDATE/SHARE was specified, UPDATE permissions are required on the
* target table, and the rewriter also adds WCOs built from the USING
* quals of UPDATE policies, using WCOs of the same kind, and this
* check enforces them too.
*/
ExecWithCheckOptions(WCO_RLS_CONFLICT_CHECK, resultRelInfo,
existing,
mtstate->ps.state);
}
/* RETURNING is required for DO SELECT */
Assert(resultRelInfo->ri_projectReturning);
*returning = ExecProcessReturning(context, resultRelInfo, false,
existing, existing, context->planSlot);
if (canSetTag)
context->estate->es_processed++;
/*
* Before releasing the existing tuple, make sure that the returning slot
* has a local copy of any pass-by-reference values.
*/
ExecMaterializeSlot(*returning);
/*
* Clear out existing tuple, as there might not be another conflict among
* the next input rows. Don't want to hold resources till the end of the
* query.
*/
ExecClearTuple(existing);
return true;
}
/*
* Perform MERGE.
*/
@@ -3549,7 +3740,7 @@ lmerge_matched:
case CMD_UPDATE:
rslot = ExecProcessReturning(context,
resultRelInfo,
CMD_UPDATE,
false,
resultRelInfo->ri_oldTupleSlot,
newslot,
context->planSlot);
@@ -3558,7 +3749,7 @@ lmerge_matched:
case CMD_DELETE:
rslot = ExecProcessReturning(context,
resultRelInfo,
CMD_DELETE,
true,
resultRelInfo->ri_oldTupleSlot,
NULL,
context->planSlot);
@@ -4329,7 +4520,8 @@ ExecModifyTable(PlanState *pstate)
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,
slot = ExecProcessReturning(&context, resultRelInfo,
operation == CMD_DELETE,
NULL, NULL, context.planSlot);
return slot;
@@ -5031,49 +5223,60 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
/*
* If needed, Initialize target list, projection and qual for ON CONFLICT
* DO UPDATE.
* For ON CONFLICT DO SELECT/UPDATE, initialize the ON CONFLICT action
* state.
*/
if (node->onConflictAction == ONCONFLICT_UPDATE)
if (node->onConflictAction == ONCONFLICT_UPDATE ||
node->onConflictAction == ONCONFLICT_SELECT)
{
OnConflictSetState *onconfl = makeNode(OnConflictSetState);
ExprContext *econtext;
TupleDesc relationDesc;
OnConflictActionState *onconfl = makeNode(OnConflictActionState);
/* already exists if created by RETURNING processing above */
if (mtstate->ps.ps_ExprContext == NULL)
ExecAssignExprContext(estate, &mtstate->ps);
econtext = mtstate->ps.ps_ExprContext;
relationDesc = resultRelInfo->ri_RelationDesc->rd_att;
/* create state for DO UPDATE SET operation */
/* action state for DO SELECT/UPDATE */
resultRelInfo->ri_onConflict = onconfl;
/* lock strength for DO SELECT [FOR UPDATE/SHARE] */
onconfl->oc_LockStrength = node->onConflictLockStrength;
/* initialize slot for the existing tuple */
onconfl->oc_Existing =
table_slot_create(resultRelInfo->ri_RelationDesc,
&mtstate->ps.state->es_tupleTable);
/*
* Create the tuple slot for the UPDATE SET projection. We want a slot
* of the table's type here, because the slot will be used to insert
* into the table, and for RETURNING processing - which may access
* system attributes.
* For ON CONFLICT DO UPDATE, initialize target list and projection.
*/
onconfl->oc_ProjSlot =
table_slot_create(resultRelInfo->ri_RelationDesc,
&mtstate->ps.state->es_tupleTable);
if (node->onConflictAction == ONCONFLICT_UPDATE)
{
ExprContext *econtext;
TupleDesc relationDesc;
/* build UPDATE SET projection state */
onconfl->oc_ProjInfo =
ExecBuildUpdateProjection(node->onConflictSet,
true,
node->onConflictCols,
relationDesc,
econtext,
onconfl->oc_ProjSlot,
&mtstate->ps);
econtext = mtstate->ps.ps_ExprContext;
relationDesc = resultRelInfo->ri_RelationDesc->rd_att;
/*
* Create the tuple slot for the UPDATE SET projection. We want a
* slot of the table's type here, because the slot will be used to
* insert into the table, and for RETURNING processing - which may
* access system attributes.
*/
onconfl->oc_ProjSlot =
table_slot_create(resultRelInfo->ri_RelationDesc,
&mtstate->ps.state->es_tupleTable);
/* build UPDATE SET projection state */
onconfl->oc_ProjInfo =
ExecBuildUpdateProjection(node->onConflictSet,
true,
node->onConflictCols,
relationDesc,
econtext,
onconfl->oc_ProjSlot,
&mtstate->ps);
}
/* initialize state to evaluate the WHERE clause, if any */
if (node->onConflictWhere)