Skip to content

Commit 47f219b

Browse files
committed
Support ON CONFLICT DO SELECT on hypertables for PostgreSQL 19
PostgreSQL 19 added INSERT ... ON CONFLICT DO SELECT, which returns the conflicting row instead of updating it and can optionally lock it. Upstream changes: Add support for INSERT ... ON CONFLICT DO SELECT. postgres/postgres@88327092ff0
1 parent 81fc99b commit 47f219b

5 files changed

Lines changed: 458 additions & 36 deletions

File tree

src/chunk_insert_state.c

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,17 @@ setup_on_conflict_state(ResultRelInfo *ht_rri, ModifyTableState *mtstate, ChunkI
230230
Relation hyper_rel = ht_rri->ri_RelationDesc;
231231
ModifyTable *mt = castNode(ModifyTable, mtstate->ps.plan);
232232

233+
/* DO UPDATE has a SET clause; DO SELECT only fetches the conflicting row. */
234+
bool do_update = mt->onConflictAction == ONCONFLICT_UPDATE;
235+
233236
OnConflictActionState *onconfl = makeNode(OnConflictActionState);
234237
memcpy(onconfl, ht_rri->ri_onConflict, sizeof(OnConflictActionState));
235238
chunk_rri->ri_onConflict = onconfl;
236239

237240
chunk_rri->ri_RootToChildMap = map;
238241
chunk_rri->ri_RootToChildMapValid = true;
239242

240-
Assert(mt->onConflictSet);
243+
Assert(!do_update || mt->onConflictSet);
241244
Assert(ht_rri->ri_onConflict != NULL);
242245

243246
/*
@@ -271,18 +274,6 @@ setup_on_conflict_state(ResultRelInfo *ht_rri, ModifyTableState *mtstate, ChunkI
271274
}
272275
else
273276
{
274-
List *onconflset;
275-
List *onconflcols;
276-
277-
/*
278-
* Translate expressions in onConflictSet to account for
279-
* different attribute numbers. For that, map partition
280-
* varattnos twice: first to catch the EXCLUDED
281-
* pseudo-relation (INNER_VAR), and second to handle the main
282-
* target relation (firstVarno).
283-
*/
284-
onconflset = copyObject(mt->onConflictSet);
285-
286277
Assert(map->outdesc == RelationGetDescr(chunk_rel));
287278

288279
if (!chunk_map)
@@ -291,33 +282,41 @@ setup_on_conflict_state(ResultRelInfo *ht_rri, ModifyTableState *mtstate, ChunkI
291282
convert_tuples_by_name(RelationGetDescr(chunk_rel), RelationGetDescr(hyper_rel));
292283
}
293284

294-
onconflset = translate_clause(onconflset, chunk_map, ht_rri->ri_RangeTableIndex, chunk_rel);
295-
296285
chunk_rri->ri_ChildToRootMap = chunk_map;
297286
chunk_rri->ri_ChildToRootMapValid = true;
298287

299-
/* Finally, adjust the target colnos to match the chunk. */
300-
if (chunk_map)
288+
if (do_update)
301289
{
290+
List *onconflset;
291+
List *onconflcols;
292+
293+
/*
294+
* Translate expressions in onConflictSet to account for
295+
* different attribute numbers. For that, map partition
296+
* varattnos twice: first to catch the EXCLUDED
297+
* pseudo-relation (INNER_VAR), and second to handle the main
298+
* target relation (firstVarno).
299+
*/
300+
onconflset = copyObject(mt->onConflictSet);
301+
onconflset =
302+
translate_clause(onconflset, chunk_map, ht_rri->ri_RangeTableIndex, chunk_rel);
303+
304+
/* Finally, adjust the target colnos to match the chunk. */
302305
onconflcols = adjust_chunk_colnos(mt->onConflictCols, chunk_rri);
303-
}
304-
else
305-
{
306-
onconflcols = mt->onConflictCols;
307-
}
308306

309-
/* create the tuple slot for the UPDATE SET projection */
310-
onconfl->oc_ProjSlot = table_slot_create(chunk_rel, NULL);
311-
state->conflproj_slot = onconfl->oc_ProjSlot;
312-
313-
/* build UPDATE SET projection state */
314-
onconfl->oc_ProjInfo = ExecBuildUpdateProjection(onconflset,
315-
true,
316-
onconflcols,
317-
RelationGetDescr(chunk_rel),
318-
mtstate->ps.ps_ExprContext,
319-
onconfl->oc_ProjSlot,
320-
&mtstate->ps);
307+
/* create the tuple slot for the UPDATE SET projection */
308+
onconfl->oc_ProjSlot = table_slot_create(chunk_rel, NULL);
309+
state->conflproj_slot = onconfl->oc_ProjSlot;
310+
311+
/* build UPDATE SET projection state */
312+
onconfl->oc_ProjInfo = ExecBuildUpdateProjection(onconflset,
313+
true,
314+
onconflcols,
315+
RelationGetDescr(chunk_rel),
316+
mtstate->ps.ps_ExprContext,
317+
onconfl->oc_ProjSlot,
318+
&mtstate->ps);
319+
}
321320

322321
Node *onconflict_where = mt->onConflictWhere;
323322

@@ -406,7 +405,11 @@ adjust_projections(ResultRelInfo *ht_rri, ModifyTableState *mtstate, ChunkInsert
406405
{
407406
set_arbiter_indexes(cis, ht_rri->ri_onConflictArbiterIndexes);
408407

409-
if (onConflictAction == ONCONFLICT_UPDATE)
408+
if (onConflictAction == ONCONFLICT_UPDATE
409+
#if PG19_GE
410+
|| onConflictAction == ONCONFLICT_SELECT
411+
#endif
412+
)
410413
{
411414
setup_on_conflict_state(ht_rri, mtstate, cis, chunk_map);
412415
}

src/nodes/modify_hypertable_exec.c

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ static bool ExecOnConflictUpdate(ModifyTableContext *context,
165165
TupleTableSlot *excludedSlot,
166166
bool canSetTag,
167167
TupleTableSlot **returning);
168+
#if PG19_GE
169+
static bool ExecOnConflictSelect(ModifyTableContext *context,
170+
ResultRelInfo *resultRelInfo,
171+
ItemPointer conflictTid,
172+
TupleTableSlot *excludedSlot,
173+
bool canSetTag,
174+
TupleTableSlot **returning);
175+
#endif
168176

169177
static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
170178
EState *estate,
@@ -850,6 +858,31 @@ ExecInsert(ModifyTableContext *context,
850858
else
851859
goto vlock;
852860
}
861+
#if PG19_GE
862+
else if (onconflict == ONCONFLICT_SELECT)
863+
{
864+
/*
865+
* In case of ON CONFLICT DO SELECT, optionally lock the
866+
* conflicting tuple, fetch it and project RETURNING on it.
867+
* Be prepared to retry if locking fails because of a
868+
* concurrent UPDATE/DELETE to the conflict tuple.
869+
*/
870+
TupleTableSlot *returning = NULL;
871+
872+
if (ExecOnConflictSelect(context,
873+
resultRelInfo,
874+
&conflictTid,
875+
slot,
876+
canSetTag,
877+
&returning))
878+
{
879+
InstrCountTuples2(&mtstate->ps, 1);
880+
return returning;
881+
}
882+
else
883+
goto vlock;
884+
}
885+
#endif
853886
else
854887
{
855888
/*
@@ -2235,6 +2268,200 @@ ExecOnConflictUpdate(ModifyTableContext *context,
22352268
return true;
22362269
}
22372270

2271+
#if PG19_GE
2272+
/*
2273+
* ExecOnConflictSelect --- execute SELECT of INSERT ON CONFLICT DO SELECT
2274+
*
2275+
* If SELECT FOR UPDATE/SHARE is specified, try to lock the conflicting tuple
2276+
* as part of speculative insertion. If the qual originating from ON CONFLICT
2277+
* DO SELECT is satisfied, project RETURNING on the conflicting row.
2278+
*
2279+
* Returns true if we're done (with or without a select), or false if the
2280+
* caller must retry the INSERT from scratch.
2281+
*
2282+
* copied and modified version of ExecOnConflictSelect from
2283+
* executor/nodeModifyTable.c
2284+
*/
2285+
static bool
2286+
ExecOnConflictSelect(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
2287+
ItemPointer conflictTid, TupleTableSlot *excludedSlot, bool canSetTag,
2288+
TupleTableSlot **returning)
2289+
{
2290+
ModifyTableState *mtstate = context->mtstate;
2291+
ExprContext *econtext = mtstate->ps.ps_ExprContext;
2292+
Relation relation = resultRelInfo->ri_RelationDesc;
2293+
ExprState *onConflictSelectWhere = resultRelInfo->ri_onConflict->oc_WhereClause;
2294+
TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing;
2295+
LockClauseStrength lockStrength = resultRelInfo->ri_onConflict->oc_LockStrength;
2296+
2297+
Assert(!resultRelInfo->ri_needLockTagTuple);
2298+
2299+
/* Fetch/lock the existing tuple, according to the requested lock strength */
2300+
if (lockStrength == LCS_NONE)
2301+
{
2302+
if (!table_tuple_fetch_row_version(relation, conflictTid, SnapshotAny, existing))
2303+
elog(ERROR, "failed to fetch conflicting tuple for ON CONFLICT");
2304+
}
2305+
else
2306+
{
2307+
LockTupleMode lockmode;
2308+
TM_FailureData tmfd;
2309+
TM_Result test;
2310+
Datum xminDatum;
2311+
TransactionId xmin;
2312+
bool isnull;
2313+
2314+
switch (lockStrength)
2315+
{
2316+
case LCS_FORKEYSHARE:
2317+
lockmode = LockTupleKeyShare;
2318+
break;
2319+
case LCS_FORSHARE:
2320+
lockmode = LockTupleShare;
2321+
break;
2322+
case LCS_FORNOKEYUPDATE:
2323+
lockmode = LockTupleNoKeyExclusive;
2324+
break;
2325+
case LCS_FORUPDATE:
2326+
lockmode = LockTupleExclusive;
2327+
break;
2328+
default:
2329+
elog(ERROR, "unexpected lock strength %d", (int) lockStrength);
2330+
}
2331+
2332+
/*
2333+
* Lock the tuple with the requested mode. A row locking conflict here
2334+
* means our previous conclusion that the tuple is conclusively
2335+
* committed is not true anymore.
2336+
*/
2337+
test = table_tuple_lock(relation,
2338+
conflictTid,
2339+
context->estate->es_snapshot,
2340+
existing,
2341+
context->estate->es_output_cid,
2342+
lockmode,
2343+
LockWaitBlock,
2344+
0,
2345+
&tmfd);
2346+
switch (test)
2347+
{
2348+
case TM_Ok:
2349+
/* success! */
2350+
break;
2351+
2352+
case TM_Invisible:
2353+
2354+
/*
2355+
* This can occur when a just inserted tuple is locked again in
2356+
* the same command, e.g. because multiple rows with the same
2357+
* conflicting key values are inserted.
2358+
*/
2359+
xminDatum = slot_getsysattr(existing, MinTransactionIdAttributeNumber, &isnull);
2360+
Assert(!isnull);
2361+
xmin = DatumGetTransactionId(xminDatum);
2362+
2363+
if (TransactionIdIsCurrentTransactionId(xmin))
2364+
ereport(ERROR,
2365+
(errcode(ERRCODE_CARDINALITY_VIOLATION),
2366+
/* translator: %s is a SQL command name */
2367+
errmsg("%s command cannot affect row a second time",
2368+
"ON CONFLICT DO SELECT"),
2369+
errhint("Ensure that no rows proposed for insertion within the same "
2370+
"command have duplicate constrained values.")));
2371+
2372+
/* This shouldn't happen */
2373+
elog(ERROR, "attempted to lock invisible tuple");
2374+
break;
2375+
2376+
case TM_SelfModified:
2377+
2378+
/*
2379+
* This state should never be reached. As a dirty snapshot is
2380+
* used to find conflicting tuples, speculative insertion
2381+
* wouldn't have seen this row to conflict with.
2382+
*/
2383+
elog(ERROR, "unexpected self-updated tuple");
2384+
break;
2385+
2386+
case TM_Updated:
2387+
if (IsolationUsesXactSnapshot())
2388+
ereport(ERROR,
2389+
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
2390+
errmsg("could not serialize access due to concurrent update")));
2391+
2392+
/*
2393+
* As long as we don't support ON CONFLICT for a partitioned
2394+
* table we shouldn't reach a case where the tuple to be locked
2395+
* is moved to another partition due to a concurrent update of
2396+
* the partition key.
2397+
*/
2398+
Assert(!ItemPointerIndicatesMovedPartitions(&tmfd.ctid));
2399+
2400+
/* Tell caller to try again from the very start. */
2401+
ExecClearTuple(existing);
2402+
return false;
2403+
2404+
case TM_Deleted:
2405+
if (IsolationUsesXactSnapshot())
2406+
ereport(ERROR,
2407+
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
2408+
errmsg("could not serialize access due to concurrent delete")));
2409+
2410+
/* see TM_Updated case */
2411+
Assert(!ItemPointerIndicatesMovedPartitions(&tmfd.ctid));
2412+
ExecClearTuple(existing);
2413+
return false;
2414+
2415+
default:
2416+
elog(ERROR, "unrecognized table_tuple_lock status: %u", test);
2417+
}
2418+
}
2419+
2420+
/*
2421+
* Verify that the tuple is visible to our MVCC snapshot if the current
2422+
* isolation level mandates that. See comments in ExecOnConflictUpdate().
2423+
*/
2424+
ExecCheckTupleVisible(context->estate, relation, existing);
2425+
2426+
/*
2427+
* Make the tuple and any needed join variables available to ExecQual. The
2428+
* EXCLUDED tuple is installed in ecxt_innertuple, while the target's
2429+
* existing tuple is installed in the scantuple. EXCLUDED has been made to
2430+
* reference INNER_VAR in setrefs.c, but there is no other redirection.
2431+
*/
2432+
econtext->ecxt_scantuple = existing;
2433+
econtext->ecxt_innertuple = excludedSlot;
2434+
econtext->ecxt_outertuple = NULL;
2435+
2436+
if (!ExecQual(onConflictSelectWhere, econtext))
2437+
{
2438+
ExecClearTuple(existing); /* see return below */
2439+
InstrCountFiltered1(&mtstate->ps, 1);
2440+
return true; /* done with the tuple */
2441+
}
2442+
2443+
if (resultRelInfo->ri_WithCheckOptions != NIL)
2444+
ExecWithCheckOptions(WCO_RLS_CONFLICT_CHECK, resultRelInfo, existing, mtstate->ps.state);
2445+
2446+
/* RETURNING is required for DO SELECT */
2447+
Assert(resultRelInfo->ri_projectReturning);
2448+
2449+
*returning =
2450+
ExecProcessReturning(resultRelInfo, CMD_INSERT, existing, existing, context->planSlot);
2451+
2452+
if (canSetTag)
2453+
context->estate->es_processed++;
2454+
2455+
/*
2456+
* Clear out the existing tuple, as there might not be another conflict
2457+
* among the next input rows. First though, make sure that the returning
2458+
* slot has a local copy of any pass-by-reference values.
2459+
*/
2460+
ExecMaterializeSlot(*returning);
2461+
ExecClearTuple(existing);
2462+
return true;
2463+
}
2464+
#endif
22382465

22392466
static void fireASTriggers(ModifyTableState *node);
22402467
static void fireBSTriggers(ModifyTableState *node);

0 commit comments

Comments
 (0)