8000 EXCLUDED expressions within ON CONFLICT UPDATE · petergeoghegan/postgres@b303364 · GitHub
[go: up one dir, main page]

Skip to content

Commit b303364

Browse files
author
Peter Geoghegan
committed
EXCLUDED expressions within ON CONFLICT UPDATE
EXCLUDED.* (which previously appeared as EXCLUDED(),and CONFLICTING() before that) is an "internal" primnode expression which enables referencing of rejected-for-insertion tuples within both the targetlist and predicate of the UPDATE portion of an INSERT ... ON CONFLICT UPDATE query. The expression is invoked using an alias-like syntax (more on how this works later). The fact that a dedicated expression is used (rather than a dedicated range table entry involved in query optimization) is an implementation detail. This additional support is particularly useful for ON CONFLICT queries that propose multiple tuples for insertion, since it isn't otherwise possible to succinctly decide which actual values to update each column with (in the event of taking the update path in respect of a given slot). The effects of BEFORE INSERT row triggers on the slot/tuple proposed for insertion are carried. This seems logical, since it might be the case that the rejected values would not have been rejected had some BEFORE INSERT trigger been disabled. On the other hand, the potential hazards around equivalent modifications occurring when both INSERT and UPDATE BEFORE triggers are fired for the same slot/tuple should be considered by client applications. It's possible to imagine a use case in which this behavior is surprising and undesirable -- essentially the same non-idempotent modification may occur twice. (It might also be the case that BEFORE trigger related side-effects undesirably occur twice, but writing BEFORE triggers with external side-effects is already considered a questionable practice for several reasons (consider commit 6868ed7), and besides, the implementation cannot reasonably prevent this, as noted in nodeModifyTable.c comments added by the main ON CONFLICT commit). In this revision, the raw grammar does not generate an ExcludedExpr. Parse analysis of ON CONFLICT UPDATE is made to add a new relation RTE to the auxiliary sub_pstate parser state (an alias for the target). This makes parse analysis build a query tree that is more or less consistent with there actually being an EXCLUDED relation. Then, as part of query rewrite, immediately after normalizing the UPDATE targetlist, Vars referencing the pseudo-relation (using the EXCLUDED alias) are replaced with ExcludedExpr that references Vars in the target relation itself. Speculative insertion/the executor arranges to rig the Vars and UPDATE-related/EPQ scan planstate's expression context such that values will actually originate from the rejected tuple's slot (driven, as always for the UPDATE's execution, by the parent INSERT ModifyTable node, changed once per slot proposed for insertion as appropriate). This whole mechanism is somewhat similar to the handling of trigger WHEN clauses, where a similar dance must also occur within the executor. Note that pg_stat_statements does not fingerprint ExludedExpr, because it cannot appear in the pre-rewrite post parse analysis query tree. (It doesn't fingerprint every primnodes anyway, mostly because some are only expected in utility statements). Other existing Node handling sites that don't expect to see primnodes that appear only after rewriting (ExcludedExpr may be in its own category here) do not have an ExcludedExpr case added either.
1 parent ba7bcd1 commit b303364

File tree

14 files changed

+380
-2
lines changed

14 files changed

+380
-2
lines changed

src/backend/executor/execQual.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ static Datum ExecEvalArrayCoerceExpr(ArrayCoerceExprState *astate,
181181
bool *isNull, ExprDoneCond *isDone);
182182
static Datum ExecEvalCurrentOfExpr(ExprState *exprstate, ExprContext *econtext,
183183
bool *isNull, ExprDoneCond *isDone);
184+
static Datum ExecEvalExcluded(ExcludedExprState *excludedExpr,
185+
ExprContext *econtext, bool *isNull,
186+
ExprDoneCond *isDone);
184187

185188

186189
/* ----------------------------------------------------------------
@@ -4301,6 +4304,56 @@ ExecEvalCurrentOfExpr(ExprState *exprstate, ExprContext *econtext,
43014304
return 0; /* keep compiler quiet */
43024305
}
43034306

4307+
/* ----------------------------------------------------------------
4308+
* ExecEvalExcluded
4309+
* ----------------------------------------------------------------
4310+
*/
4311+
static Datum
4312+
ExecEvalExcluded(ExcludedExprState *excludedExpr, ExprContext *econtext,
4313+
bool *isNull, ExprDoneCond *isDone)
4314+
{
4315+
/*
4316+
* ExcludedExpr is essentially an expression that adapts its single Var
4317+
* argument to refer to the expression context inner slot's tuple, which is
4318+
* reserved for the purpose of referencing EXCLUDED.* tuples within ON
4319+
* CONFLICT UPDATE auxiliary queries' EPQ expression context (ON CONFLICT
4320+
* UPDATE makes special use of the EvalPlanQual() mechanism to update).
4321+
*
4322+
* nodeModifyTable.c assigns its own table slot in the auxiliary queries'
4323+
* EPQ expression state (originating in the parent INSERT node) on the
4324+
* assumption that it may only be used by ExcludedExpr, and on the
4325+
* assumption that the inner slot is not otherwise useful. This occurs in
4326+
* advance of the expression evaluation for UPDATE (which calls here are
4327+
* part of) once per slot proposed for insertion, and works because of
4328+
* restrictions on the structure of ON CONFLICT UPDATE auxiliary queries.
4329+
*
4330+
* On assert-enabled builds, call function verifying that the exprcontext
4331+
* outer tuple slot has a descriptor that matches the same exprcontext's
4332+
* scan tuple in structure. Our assumption is that the executor has
4333+
* assigned an EXCLUDED.* slot containing a tuple originally proposed for
4334+
* insertion (with the effects of BEFORE ROW INSERT triggers carried).
4335+
*/
4336+
4337+
#ifdef USE_ASSERT_CHECKING
4338+
/*
4339+
* The pair of descriptors should match, by the same standard that function
4340+
* result tuple type is forced to match what a calling query expects (e.g.
4341+
* there must be the same number of columns, and corresponding pairs of
4342+
* columns must be binary coercible). This is a slightly looser standard
4343+
* than what is expected, in that attribute types should match exactly and
4344+
* not just be binary coercible, but it's a convenient check to make.
4345+
*
4346+
* If tupledesc_match() complains, at the very least, the varno adjustment
4347+
* for this case within ExecInitExpr() is completely broken. Failing this
4348+
* is a "can't happen" condition, and so we don't bother making sure any
4349+
* error tupledesc_match() might throw describes the failure precisely.
4350+
*/
4351+
tupledesc_match(econtext->ecxt_scantuple->tts_tupleDescriptor,
4352+
econtext->ecxt_innertuple->tts_tupleDescriptor);
4353+
#endif
4354+
/* Just evaluate nested Var */
4355+
return ExecEvalScalarVar(excludedExpr->arg, econtext, isNull, isDone);
4356+
}
43044357

43054358
/*
43064359
* ExecEvalExprSwitchContext
@@ -5026,6 +5079,30 @@ ExecInitExpr(Expr *node, PlanState *parent)
50265079
state = (ExprState *) makeNode(ExprState);
50275080
state->evalfunc = ExecEvalCurrentOfExpr;
50285081
break;
5082+
case T_ExcludedExpr:
5083+
{
5084+
ExcludedExpr *excludedexpr = (ExcludedExpr *) node;
5085+
ExcludedExprState *cstate = makeNode(ExcludedExprState);
5086+
Var *contained = (Var*) excludedexpr->arg;
5087+
5088+
/*
5089+
* varno forced to INNER_VAR -- see remarks within
5090+
* ExecLockUpdateTuple().
5091+
*
5092+
* We rely on the assumption that the only place that
5093+
* ExcludedExpr may appear is where EXCLUDED Var references
5094+
* originally appeared after parse analysis. The rewriter
5095+
* replaces these with ExcludedExpr that reference the
5096+
* corresponding Var within the ON CONFLICT UPDATE target RTE.
5097+
*/
5098+
Assert(IsA(contained, Var));
5099+
5100+
contained->varno = INNER_VAR;
5101+
cstate->arg = ExecInitExpr((Expr *) contained, parent);
5102+
state = (ExprState *) cstate;
5103+
state->evalfunc = (ExprStateEvalFunc) ExecEvalExcluded;
5104+
}
5105+
break;
50295106
case T_TargetEntry:
50305107
{
50315108
TargetEntry *tle = (TargetEntry *) node;

src/backend/executor/nodeModifyTable.c

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ static bool ExecLockUpdateTuple(EState *estate,
5858
ResultRelInfo *relinfo,
5959
ItemPointer tid,
6060
TupleTableSlot *planSlot,
61+
TupleTableSlot *insertSlot,
6162
ModifyTableState *onConflict);
6263

6364
/*
@@ -411,7 +412,7 @@ ExecInsert(TupleTableSlot *slot,
411412

412413
/* If failure to lock/update, restart */
413414
if (!ExecLockUpdateTuple(estate, resultRelInfo, &conflictTid,
414-
planSlot, onConflict))
415+
planSlot, slot, onConflict))
415416
goto retry;
416417
}
417418
else if (spec == SPEC_IGNORE)
@@ -978,6 +979,7 @@ ExecLockUpdateTuple(EState *estate,
978979
ResultRelInfo *relinfo,
979980
ItemPointer tid,
980981
TupleTableSlot *planSlot,
982+
TupleTableSlot *insertSlot,
981983
ModifyTableState *onConflict)
982984
{
983985
Relation relation = relinfo->ri_RelationDesc;
@@ -987,6 +989,7 @@ ExecLockUpdateTuple(EState *estate,
987989
HTSU_Result test;
988990
Buffer buffer;
989991
TupleTableSlot *slot;
992+
ExprContext *econtext;
990993

991994
Assert(ItemPointerIsValid(tid));
992995

@@ -1114,9 +1117,31 @@ ExecLockUpdateTuple(EState *estate,
11141117
*/
11151118
EvalPlanQualBegin(&onConflict->mt_epqstate, onConflict->ps.state);
11161119

1120+
/*
1121+
* Save EPQ expression context. Auxiliary plan's scan node (which
1122+
* would have been just initialized by EvalPlanQualBegin() on the
1123+
* first time through here per query) cannot fail to provide one.
1124+
*/
1125+
econtext = onConflict->mt_epqstate.planstate->ps_ExprContext;
1126+
11171127
/* Range table index should match parent plan */
11181128
EvalPlanQualSetTuple(&onConflict->mt_epqstate,
11191129
relinfo->ri_RangeTableIndex, copyTuple);
1130+
1131+
/*
1132+
* Make available rejected tuple for referencing within UPDATE
1133+
* expression (that is, make available a slot with rejected
1134+
* heaptuple, possibly already modified by BEFORE INSERT triggers).
1135+
* This is for the benefit of any ExcludedExpr that may appear
1136+
* within UPDATE's targetlist or predicate. The rejected tuple may
1137+
* be referenced as an ExcludedExpr Var, specially modified to have
1138+
* an INNER_VAR varno on the assumption that the inner slot of the
1139+
* EPQ scan plan state's expression context will contain the
1140+
* rejected heaptuple. See handling of this within handleRewrite.c
1141+
* and execQual.c.
1142+
*/
1143+
econtext->ecxt_innertuple = insertSlot;
1144+
11201145
slot = EvalPlanQualNext(&onConflict->mt_epqstate);
11211146

11221147
if (!TupIsNull(slot))

src/backend/nodes/copyfuncs.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,6 +1771,19 @@ _copyCurrentOfExpr(const CurrentOfExpr *from)
17711771
return newnode;
17721772
}
17731773

1774+
/*
1775+
* _copyExcludedExpr
1776+
*/
1777+
static ExcludedExpr *
1778+
_copyExcludedExpr(const ExcludedExpr *from)
1779+
{
1780+
ExcludedExpr *newnode = makeNode(ExcludedExpr);
1781+
1782+
COPY_NODE_FIELD(arg);
1783+
1784+
return newnode;
1785+
}
1786+
17741787
/*
17751788
* _copyTargetEntry
17761789
*/
@@ -4268,6 +4281,9 @@ copyObject(const void *from)
42684281
case T_CurrentOfExpr:
42694282
retval = _copyCurrentOfExpr(from);
42704283
break;
4284+
case T_ExcludedExpr:
4285+
retval = _copyExcludedExpr(from);
4286+
break;
42714287
case T_TargetEntry:
42724288
retval = _copyTargetEntry(from);
42734289
break;

src/backend/nodes/equalfuncs.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,14 @@ _equalCurrentOfExpr(const CurrentOfExpr *a, const CurrentOfExpr *b)
680680
return true;
681681
}
682682

683+
static bool
684+
_equalExcludedExpr(const ExcludedExpr *a, const ExcludedExpr *b)
685+
{
686+
COMPARE_NODE_FIELD(arg);
687+
688+
return true;
689+
}
690+
683691
static bool
684692
_equalTargetEntry(const TargetEntry *a, const TargetEntry *b)
685693
{
@@ -2709,6 +2717,9 @@ equal(const void *a, const void *b)
27092717
case T_CurrentOfExpr:
27102718
retval = _equalCurrentOfExpr(a, b);
27112719
break;
2720+
case T_ExcludedExpr:
2721+
retval = _equalExcludedExpr(a, b);
2722+
break;
27122723
case T_TargetEntry:
27132724
retval = _equalTargetEntry(a, b);
27142725
break;

src/backend/nodes/nodeFuncs.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,13 @@ exprType(const Node *expr)
235235
case T_CurrentOfExpr:
236236
type = BOOLOID;
237237
break;
238+
case T_ExcludedExpr:
239+
{
240+
const ExcludedExpr *n = (const ExcludedExpr *) expr;
241+
242+
type = exprType((Node *) n->arg);
243+
}
244+
break;
238245
case T_PlaceHolderVar:
239246
type = exprType((Node *) ((const PlaceHolderVar *) expr)->phexpr);
240247
break;
@@ -469,6 +476,12 @@ exprTypmod(const Node *expr)
469476
return ((const CoerceToDomainValue *) expr)->typeMod;
470477
case T_SetToDefault:
471478
return ((const SetToDefault *) expr)->typeMod;
479+
case T_ExcludedExpr:
480+
{
481+
const ExcludedExpr *n = (const ExcludedExpr *) expr;
482+
483+
return ((const Var *) n->arg)->vartypmod;
484+
}
472485
case T_PlaceHolderVar:
473486
return exprTypmod((Node *) ((const PlaceHolderVar *) expr)->phexpr);
474487
default:
@@ -894,6 +907,9 @@ exprCollation(const Node *expr)
894907
case T_CurrentOfExpr:
895908
coll = InvalidOid; /* result is always boolean */
896909
break;
910+
case T_ExcludedExpr:
911+
coll = exprCollation((Node *) ((const ExcludedExpr *) expr)->arg);
912+
break;
897913
case T_PlaceHolderVar:
898914
coll = exprCollation((Node *) ((const PlaceHolderVar *) expr)->phexpr);
899915
break;
@@ -1089,6 +1105,12 @@ exprSetCollation(Node *expr, Oid collation)
10891105
case T_CurrentOfExpr:
10901106
Assert(!OidIsValid(collation)); /* result is always boolean */
10911107
break;
1108+
case T_ExcludedExpr:
1109+
{
1110+
Var *v = (Var *) ((ExcludedExpr *) expr)->arg;
1111+
v->varcollid = collation;
1112+
}
1113+
break;
10921114
default:
10931115
elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
10941116
break;
@@ -1484,6 +1506,10 @@ exprLocation(const Node *expr)
14841506
/* just use argument's location */
14851507
loc = exprLocation((Node *) ((const PlaceHolderVar *) expr)->phexpr);
14861508
break;
1509+
case T_ExcludedExpr:
1510+
/* just use nested expr's location */
1511+
loc = exprLocation((Node *) ((const ExcludedExpr *) expr)->arg);
1512+
break;
14871513
default:
14881514
/* for any other node type it's just unknown... */
14891515
loc = -1;
@@ -1913,6 +1939,8 @@ expression_tree_walker(Node *node,
19131939
break;
19141940
case T_PlaceHolderVar:
19151941
return walker(((PlaceHolderVar *) node)->phexpr, context);
1942+
case T_ExcludedExpr:
1943+
return walker(((ExcludedExpr *) node)->arg, context);
19161944
case T_AppendRelInfo:
19171945
{
19181946
AppendRelInfo *appinfo = (AppendRelInfo *) node;
@@ -2627,6 +2655,16 @@ expression_tree_mutator(Node *node,
26272655
return (Node *) newnode;
26282656
}
26292657
break;
2658+
case T_ExcludedExpr:
2659+
{
2660+
ExcludedExpr *excludedexpr = (ExcludedExpr *) node;
2661+
ExcludedExpr *newnode;
2662+
2663+
FLATCOPY(newnode, excludedexpr, ExcludedExpr);
2664+
MUTATE(newnode->arg, newnode->arg, Node *);
2665+
return (Node *) newnode;
2666+
}
2667+
break;
26302668
case T_AppendRelInfo:
26312669
{
26322670
AppendRelInfo *appinfo = (AppendRelInfo *) node;

src/backend/nodes/outfuncs.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,14 @@ _outCurrentOfExpr(StringInfo str, const CurrentOfExpr *node)
14261426
WRITE_INT_FIELD(cursor_param);
14271427
}
14281428

1429+
static void
1430+
_outExcludedExpr(StringInfo str, const ExcludedExpr *node)
1431+
{
1432+
WRITE_NODE_TYPE("EXCLUDED");
1433+
1434+
WRITE_NODE_FIELD(arg);
1435+
}
1436+
14291437
static void
14301438
_outTargetEntry(StringInfo str, const TargetEntry *node)
14311439
{
@@ -3062,6 +3070,9 @@ _outNode(StringInfo str, const void *obj)
30623070
case T_CurrentOfExpr:
30633071
_outCurrentOfExpr(str, obj);
30643072
break;
3073+
case T_ExcludedExpr:
3074+
_outExcludedExpr(str, obj);
3075+
break;
30653076
case T_TargetEntry:
30663077
_outTargetEntry(str, obj);
30673078
break;

src/backend/nodes/readfuncs.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,19 @@ _readCurrentOfExpr(void)
11301130
READ_DONE();
11311131
}
11321132

1133+
/*
1134+
* _readExcludedExpr
1135+
*/
1136+
static ExcludedExpr *
1137+
_readExcludedExpr(void)
1138+
{
1139+
READ_LOCALS(ExcludedExpr);
1140+
1141+
READ_NODE_FIELD(arg);
1142+
1143+
READ_DONE();
1144+
}
1145+
11331146
/*
11341147
* _readTargetEntry
11351148
*/
@@ -1395,6 +1408,8 @@ parseNodeString(void)
13951408
return_value = _readSetToDefault();
13961409
else if (MATCH("CURRENTOFEXPR", 13))
13971410
return_value = _readCurrentOfExpr();
1411+
else if (MATCH("EXCLUDED", 8))
1412+
return_value = _readExcludedExpr();
13981413
else if (MATCH("TARGETENTRY", 11))
13991414
return_value = _readTargetEntry();
14001415
else if (MATCH("RANGETBLREF", 11))

src/backend/optimizer/plan/setrefs.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,11 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
791791

792792
if (splan->onConflictPlan)
793793
{
794+
/*
795+
* Decrement rtoffset, to compensate for dummy RTE left by
796+
* EXCLUDED.*
797+
*/
798+
rtoffset -= PRS2_OLD_VARNO;
794799
splan->onConflictPlan = (Plan *) set_plan_refs(root,
795800
(Plan *) splan->onConflictPlan,
796801
rtoffset);

0 commit comments

Comments
 (0)
0