Skip to content

Commit 39f180f

Browse files
committed
Fix creation of resjunk tlist entries for inherited mixed UPDATE/DELETE.
rewriteTargetListUD's processing is dependent on the relkind of the query's target table. That was fine at the time it was made to act that way, even for queries on inheritance trees, because all tables in an inheritance tree would necessarily be plain tables. However, the 9.5 feature addition allowing some members of an inheritance tree to be foreign tables broke the assumption that rewriteTargetListUD's output tlist could be applied to all child tables with nothing more than column-number mapping. This led to visible failures if foreign child tables had row-level triggers, and would also break in cases where child tables belonged to FDWs that used methods other than CTID for row identification. To fix, delay running rewriteTargetListUD until after the planner has expanded inheritance, so that it is applied separately to the (already mapped) tlist for each child table. We can conveniently call it from preprocess_targetlist. Refactor associated code slightly to avoid the need to heap_open the target relation multiple times during preprocess_targetlist. (The APIs remain a bit ugly, particularly around the point of which steps scribble on parse->targetList and which don't. But avoiding such scribbling would require a change in FDW callback APIs, which is more pain than it's worth.) Also fix ExecModifyTable to ensure that "tupleid" is reset to NULL when we transition from rows providing a CTID to rows that don't. (That's really an independent bug, but it manifests in much the same cases.) Add a regression test checking one manifestation of this problem, which was that row-level triggers on a foreign child table did not work right. Back-patch to 9.5 where the problem was introduced. Etsuro Fujita, reviewed by Ildus Kurbangaliev and Ashutosh Bapat Discussion: https://postgr.es/m/20170514150525.0346ba72@postgrespro.ru
1 parent d3aeaba commit 39f180f

File tree

10 files changed

+202
-99
lines changed

10 files changed

+202
-99
lines changed

contrib/postgres_fdw/expected/postgres_fdw.out

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3592,6 +3592,71 @@ fetch from c;
35923592
update bar set f2 = null where current of c;
35933593
ERROR: WHERE CURRENT OF is not supported for this table type
35943594
rollback;
3595+
-- Test that UPDATE/DELETE with inherited target works with row-level triggers
3596+
CREATE TRIGGER trig_row_before
3597+
BEFORE UPDATE OR DELETE ON bar2
3598+
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
3599+
CREATE TRIGGER trig_row_after
3600+
AFTER UPDATE OR DELETE ON bar2
3601+
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
3602+
explain (verbose, costs off)
3603+
update bar set f2 = f2 + 100;
3604+
QUERY PLAN
3605+
--------------------------------------------------------------------------------------
3606+
Update on public.bar
3607+
Update on public.bar
3608+
Foreign Update on public.bar2
3609+
Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1 RETURNING f1, f2, f3
3610+
-> Seq Scan on public.bar
3611+
Output: bar.f1, (bar.f2 + 100), bar.ctid
3612+
-> Foreign Scan on public.bar2
3613+
Output: bar2.f1, (bar2.f2 + 100), bar2.f3, bar2.ctid, bar2.*
3614+
Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
3615+
(9 rows)
3616+
3617+
update bar set f2 = f2 + 100;
3618+
NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2
3619+
NOTICE: OLD: (3,233,33),NEW: (3,333,33)
3620+
NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2
3621+
NOTICE: OLD: (4,244,44),NEW: (4,344,44)
3622+
NOTICE: trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2
3623+
NOTICE: OLD: (7,177,77),NEW: (7,277,77)
3624+
NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2
3625+
NOTICE: OLD: (3,233,33),NEW: (3,333,33)
3626+
NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2
3627+
NOTICE: OLD: (4,244,44),NEW: (4,344,44)
3628+
NOTICE: trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2
3629+
NOTICE: OLD: (7,177,77),NEW: (7,277,77)
3630+
explain (verbose, costs off)
3631+
delete from bar where f2 < 400;
3632+
QUERY PLAN
3633+
---------------------------------------------------------------------------------------------
3634+
Delete on public.bar
3635+
Delete on public.bar
3636+
Foreign Delete on public.bar2
3637+
Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 RETURNING f1, f2, f3
3638+
-> Seq Scan on public.bar
3639+
Output: bar.ctid
3640+
Filter: (bar.f2 < 400)
3641+
-> Foreign Scan on public.bar2
3642+
Output: bar2.ctid, bar2.*
3643+
Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE
3644+
(10 rows)
3645+
3646+
delete from bar where f2 < 400;
3647+
NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON bar2
3648+
NOTICE: OLD: (3,333,33)
3649+
NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON bar2
3650+
NOTICE: OLD: (4,344,44)
3651+
NOTICE: trig_row_before(23, skidoo) BEFORE ROW DELETE ON bar2
3652+
NOTICE: OLD: (7,277,77)
3653+
NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON bar2
3654+
NOTICE: OLD: (3,333,33)
3655+
NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON bar2
3656+
NOTICE: OLD: (4,344,44)
3657+
NOTICE: trig_row_after(23, skidoo) AFTER ROW DELETE ON bar2
3658+
NOTICE: OLD: (7,277,77)
3659+
-- cleanup
35953660
drop table foo cascade;
35963661
NOTICE: drop cascades to foreign table foo2
35973662
drop table bar cascade;

contrib/postgres_fdw/sql/postgres_fdw.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,24 @@ fetch from c;
825825
update bar set f2 = null where current of c;
826826
rollback;
827827

828+
-- Test that UPDATE/DELETE with inherited target works with row-level triggers
829+
CREATE TRIGGER trig_row_before
830+
BEFORE UPDATE OR DELETE ON bar2
831+
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
832+
833+
CREATE TRIGGER trig_row_after
834+
AFTER UPDATE OR DELETE ON bar2
835+
FOR EACH ROW EXECUTE PROCEDURE trigger_data(23,'skidoo');
836+
837+
explain (verbose, costs off)
838+
update bar set f2 = f2 + 100;
839+
update bar set f2 = f2 + 100;
840+
841+
explain (verbose, costs off)
842+
delete from bar where f2 < 400;
843+
delete from bar where f2 < 400;
844+
845+
-- cleanup
828846
drop table foo cascade;
829847
drop table bar cascade;
830848
drop table loct1;

doc/src/sgml/fdwhandler.sgml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,11 +382,14 @@ AddForeignUpdateTargets (Query *parsetree,
382382
<literal>wholerow</literal>, or
383383
<literal>wholerow<replaceable>N</></literal>, as the core system can
384384
generate junk columns of these names.
385+
If the extra expressions are more complex than simple Vars, they
386+
must be run through <function>eval_const_expressions</function>
387+
before adding them to the targetlist.
385388
</para>
386389

387390
<para>
388-
This function is called in the rewriter, not the planner, so the
389-
information available is a bit different from that available to the
391+
Although this function is called during planning, the
392+
information provided is a bit different from that available to other
390393
planning routines.
391394
<literal>parsetree</> is the parse tree for the <command>UPDATE</> or
392395
<command>DELETE</> command, while <literal>target_rte</> and

doc/src/sgml/rules.sgml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,12 @@
167167

168168
<para>
169169
<command>DELETE</command> commands don't need a normal target list
170-
because they don't produce any result. Instead, the rule system
170+
because they don't produce any result. Instead, the planner
171171
adds a special <acronym>CTID</> entry to the empty target list,
172172
to allow the executor to find the row to be deleted.
173173
(<acronym>CTID</> is added when the result relation is an ordinary
174-
table. If it is a view, a whole-row variable is added instead,
175-
as described in <xref linkend="rules-views-update">.)
174+
table. If it is a view, a whole-row variable is added instead, by
175+
the rule system, as described in <xref linkend="rules-views-update">.)
176176
</para>
177177

178178
<para>
@@ -194,7 +194,7 @@
194194
column = expression</literal> part of the command. The planner will
195195
handle missing columns by inserting expressions that copy the values
196196
from the old row into the new one. Just as for <command>DELETE</>,
197-
the rule system adds a <acronym>CTID</> or whole-row variable so that
197+
a <acronym>CTID</> or whole-row variable is added so that
198198
the executor can identify the old row to be updated.
199199
</para>
200200

src/backend/executor/nodeModifyTable.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1310,7 +1310,7 @@ ExecModifyTable(ModifyTableState *node)
13101310
JunkFilter *junkfilter;
13111311
TupleTableSlot *slot;
13121312
TupleTableSlot *planSlot;
1313-
ItemPointer tupleid = NULL;
1313+
ItemPointer tupleid;
13141314
ItemPointerData tuple_ctid;
13151315
HeapTupleData oldtupdata;
13161316
HeapTuple oldtuple;
@@ -1398,6 +1398,7 @@ ExecModifyTable(ModifyTableState *node)
13981398
EvalPlanQualSetSlot(&node->mt_epqstate, planSlot);
13991399
slot = planSlot;
14001400

1401+
tupleid = NULL;
14011402
oldtuple = NULL;
14021403
if (junkfilter != NULL)
14031404
{

src/backend/optimizer/plan/planner.c

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,7 +1200,7 @@ inheritance_planner(PlannerInfo *root)
12001200
/* although dummy, it must have a valid tlist for executor */
12011201
List *tlist;
12021202

1203-
tlist = preprocess_targetlist(root, parse->targetList);
1203+
tlist = preprocess_targetlist(root);
12041204
return (Plan *) make_result(root,
12051205
tlist,
12061206
(Node *) list_make1(makeBoolConst(false,
@@ -1262,7 +1262,7 @@ static Plan *
12621262
grouping_planner(PlannerInfo *root, double tuple_fraction)
12631263
{
12641264
Query *parse = root->parse;
1265-
List *tlist = parse->targetList;
1265+
List *tlist;
12661266
int64 offset_est = 0;
12671267
int64 count_est = 0;
12681268
double limit_tuples = -1.0;
@@ -1327,7 +1327,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
13271327
Assert(parse->commandType == CMD_SELECT);
13281328

13291329
tlist = postprocess_setop_tlist(copyObject(result_plan->targetlist),
1330-
tlist);
1330+
parse->targetList);
13311331

13321332
/*
13331333
* Can't handle FOR [KEY] UPDATE/SHARE here (parser should have
@@ -1364,7 +1364,6 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
13641364
bool use_hashed_grouping = false;
13651365
WindowFuncLists *wflists = NULL;
13661366
List *activeWindows = NIL;
1367-
OnConflictExpr *onconfl;
13681367
int maxref = 0;
13691368
int *tleref_to_colnum_map;
13701369
List *rollup_lists = NIL;
@@ -1453,14 +1452,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction)
14531452
numGroupCols = list_length(parse->groupClause);
14541453

14551454
/* Preprocess targetlist */
1456-
tlist = preprocess_targetlist(root, tlist);
1457-
1458-
onconfl = parse->onConflict;
1459-
if (onconfl)
1460-
onconfl->onConflictSet =
1461-
preprocess_onconflict_targetlist(onconfl->onConflictSet,
1462-
parse->resultRelation,
1463-
parse->rtable);
1455+
tlist = preprocess_targetlist(root);
14641456

14651457
/*
14661458
* Expand any rangetable entries that have security barrier quals.

src/backend/optimizer/prep/preptlist.c

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,29 @@
44
* Routines to preprocess the parse tree target list
55
*
66
* For INSERT and UPDATE queries, the targetlist must contain an entry for
7-
* each attribute of the target relation in the correct order. For all query
7+
* each attribute of the target relation in the correct order. For UPDATE and
8+
* DELETE queries, it must also contain junk tlist entries needed to allow the
9+
* executor to identify the rows to be updated or deleted. For all query
810
* types, we may need to add junk tlist entries for Vars used in the RETURNING
911
* list and row ID information needed for SELECT FOR UPDATE locking and/or
1012
* EvalPlanQual checking.
1113
*
12-
* NOTE: the rewriter's rewriteTargetListIU and rewriteTargetListUD
13-
* routines also do preprocessing of the targetlist. The division of labor
14-
* between here and there is a bit arbitrary and historical.
14+
* The query rewrite phase also does preprocessing of the targetlist (see
15+
* rewriteTargetListIU). The division of labor between here and there is
16+
* partially historical, but it's not entirely arbitrary. In particular,
17+
* consider an UPDATE across an inheritance tree. What rewriteTargetListIU
18+
* does need be done only once (because it depends only on the properties of
19+
* the parent relation). What's done here has to be done over again for each
20+
* child relation, because it depends on the properties of the child, which
21+
* might be of a different relation type, or have more columns and/or a
22+
* different column order than the parent.
23+
*
24+
* The fact that rewriteTargetListIU sorts non-resjunk tlist entries by column
25+
* position, which expand_targetlist depends on, violates the above comment
26+
* because the sorting is only valid for the parent relation. In inherited
27+
* UPDATE cases, adjust_inherited_tlist runs in between to take care of fixing
28+
* the tlists for child tables to keep expand_targetlist happy. We do it like
29+
* that because it's faster in typical non-inherited cases.
1530
*
1631
*
1732
* Portions Copyright (c) 1996-2015, PostgreSQL Global Development Group
@@ -33,48 +48,74 @@
3348
#include "optimizer/tlist.h"
3449
#include "parser/parsetree.h"
3550
#include "parser/parse_coerce.h"
51+
#include "rewrite/rewriteHandler.h"
3652
#include "utils/rel.h"
3753

3854

3955
static List *expand_targetlist(List *tlist, int command_type,
40-
Index result_relation, List *range_table);
56+
Index result_relation, Relation rel);
4157

4258

4359
/*
4460
* preprocess_targetlist
4561
* Driver for preprocessing the parse tree targetlist.
4662
*
4763
* Returns the new targetlist.
64+
*
65+
* As a side effect, if there's an ON CONFLICT UPDATE clause, its targetlist
66+
* is also preprocessed (and updated in-place).
4867
*/
4968
List *
50-
preprocess_targetlist(PlannerInfo *root, List *tlist)
69+
preprocess_targetlist(PlannerInfo *root)
5170
{
5271
Query *parse = root->parse;
5372
int result_relation = parse->resultRelation;
5473
List *range_table = parse->rtable;
5574
CmdType command_type = parse->commandType;
75+
RangeTblEntry *target_rte = NULL;
76+
Relation target_relation = NULL;
77+
List *tlist;
5678
ListCell *lc;
5779

5880
/*
59-
* Sanity check: if there is a result relation, it'd better be a real
60-
* relation not a subquery. Else parser or rewriter messed up.
81+
* If there is a result relation, open it so we can look for missing
82+
* columns and so on. We assume that previous code already acquired at
83+
* least AccessShareLock on the relation, so we need no lock here.
6184
*/
6285
if (result_relation)
6386
{
64-
RangeTblEntry *rte = rt_fetch(result_relation, range_table);
87+
target_rte = rt_fetch(result_relation, range_table);
88+
89+
/*
90+
* Sanity check: it'd better be a real relation not, say, a subquery.
91+
* Else parser or rewriter messed up.
92+
*/
93+
if (target_rte->rtekind != RTE_RELATION)
94+
elog(ERROR, "result relation must be a regular relation");
6595

66-
if (rte->subquery != NULL || rte->relid == InvalidOid)
67-
elog(ERROR, "subquery cannot be result relation");
96+
target_relation = heap_open(target_rte->relid, NoLock);
6897
}
98+
else
99+
Assert(command_type == CMD_SELECT);
100+
101+
/*
102+
* For UPDATE/DELETE, add any junk column(s) needed to allow the executor
103+
* to identify the rows to be updated or deleted. Note that this step
104+
* scribbles on parse->targetList, which is not very desirable, but we
105+
* keep it that way to avoid changing APIs used by FDWs.
106+
*/
107+
if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
108+
rewriteTargetListUD(parse, target_rte, target_relation);
69109

70110
/*
71111
* for heap_form_tuple to work, the targetlist must match the exact order
72112
* of the attributes. We also need to fill in any missing attributes. -ay
73113
* 10/94
74114
*/
115+
tlist = parse->targetList;
75116
if (command_type == CMD_INSERT || command_type == CMD_UPDATE)
76117
tlist = expand_targetlist(tlist, command_type,
77-
result_relation, range_table);
118+
result_relation, target_relation);
78119

79120
/*
80121
* Add necessary junk columns for rowmarked rels. These values are needed
@@ -178,19 +219,21 @@ preprocess_targetlist(PlannerInfo *root, List *tlist)
178219
list_free(vars);
179220
}
180221

181-
return tlist;
182-
}
222+
/*
223+
* If there's an ON CONFLICT UPDATE clause, preprocess its targetlist too
224+
* while we have the relation open.
225+
*/
226+
if (parse->onConflict)
227+
parse->onConflict->onConflictSet =
228+
expand_targetlist(parse->onConflict->onConflictSet,
229+
CMD_UPDATE,
230+
result_relation,
231+
target_relation);
183232

184-
/*
185-
* preprocess_onconflict_targetlist
186-
* Process ON CONFLICT SET targetlist.
187-
*
188-
* Returns the new targetlist.
189-
*/
190-
List *
191-
preprocess_onconflict_targetlist(List *tlist, int result_relation, List *range_table)
192-
{
193-
return expand_targetlist(tlist, CMD_UPDATE, result_relation, range_table);
233+
if (target_relation)
234+
heap_close(target_relation, NoLock);
235+
236+
return tlist;
194237
}
195238

196239

@@ -208,11 +251,10 @@ preprocess_onconflict_targetlist(List *tlist, int result_relation, List *range_t
208251
*/
209252
static List *
210253
expand_targetlist(List *tlist, int command_type,
211-
Index result_relation, List *range_table)
254+
Index result_relation, Relation rel)
212255
{
213256
List *new_tlist = NIL;
214257
ListCell *tlist_item;
215-
Relation rel;
216258
int attrno,
217259
numattrs;
218260

@@ -223,12 +265,8 @@ expand_targetlist(List *tlist, int command_type,
223265
* order; but we have to insert TLEs for any missing attributes.
224266
*
225267
* Scan the tuple description in the relation's relcache entry to make
226-
* sure we have all the user attributes in the right order. We assume
227-
* that the rewriter already acquired at least AccessShareLock on the
228-
* relation, so we need no lock here.
268+
* sure we have all the user attributes in the right order.
229269
*/
230-
rel = heap_open(getrelid(result_relation, range_table), NoLock);
231-
232270
numattrs = RelationGetNumberOfAttributes(rel);
233271

234272
for (attrno = 1; attrno <= numattrs; attrno++)
@@ -371,8 +409,6 @@ expand_targetlist(List *tlist, int command_type,
371409
tlist_item = lnext(tlist_item);
372410
}
373411

374-
heap_close(rel, NoLock);
375-
376412
return new_tlist;
377413
}
378414

0 commit comments

Comments
 (0)