Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit d88d8ad

Browse files
committed
Fix corner case for a BEFORE ROW UPDATE trigger returning OLD.
If the old row has any "missing" attributes that are supposed to be retrieved from an associated tuple descriptor, the wrong things happened because the trigger result is shoved directly into an executor slot that lacks the missing-attribute data. Notably, CHECK-constraint verification would incorrectly see those columns as NULL, and so would RETURNING-list evaluation. Band-aid around this by forcibly expanding the tuple before passing it to the trigger function. (IMO it was a fundamental misdesign to put the missing-attribute data into tuple constraints, which so much of the system considers to be optional. But we're probably stuck with that now, and will have to continue to apply band-aids as we find other places with similar issues.) Back-patch to v12. v11 would also have the issue, except that commit 920311a already applied a similar band-aid. That forced expansion in more cases than seem really necessary, though, so this isn't a directly equivalent fix. Amit Langote, with some cosmetic changes by me Discussion: https://postgr.es/m/16644-5da7ef98a7ac4545@postgresql.org
1 parent 563973b commit d88d8ad

File tree

3 files changed

+90
-1
lines changed

3 files changed

+90
-1
lines changed

src/backend/commands/trigger.c

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ static bool GetTupleForTrigger(EState *estate,
8888
LockTupleMode lockmode,
8989
TupleTableSlot *oldslot,
9090
TupleTableSlot **newSlot);
91+
static HeapTuple MaterializeTupleForTrigger(TupleTableSlot *slot,
92+
bool *shouldFree);
9193
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
9294
Trigger *trigger, TriggerEvent event,
9395
Bitmapset *modifiedCols,
@@ -2671,7 +2673,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
26712673
ExecCopySlot(newslot, epqslot_clean);
26722674
}
26732675

2674-
trigtuple = ExecFetchSlotHeapTuple(oldslot, true, &should_free_trig);
2676+
trigtuple = MaterializeTupleForTrigger(oldslot, &should_free_trig);
26752677
}
26762678
else
26772679
{
@@ -2924,6 +2926,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
29242926
}
29252927

29262928

2929+
/*
2930+
* Fetch tuple into "oldslot", dealing with locking and EPQ if necessary
2931+
*/
29272932
static bool
29282933
GetTupleForTrigger(EState *estate,
29292934
EPQState *epqstate,
@@ -3037,6 +3042,40 @@ GetTupleForTrigger(EState *estate,
30373042
return true;
30383043
}
30393044

3045+
/*
3046+
* Extract a HeapTuple that we can pass off to trigger functions.
3047+
*
3048+
* We must materialize the tuple and make sure it is not dependent on any
3049+
* attrmissing data. This is needed for the old row in BEFORE UPDATE
3050+
* triggers, since they can choose to pass back this exact tuple as the update
3051+
* result, causing the tuple to be inserted into an executor slot that lacks
3052+
* the attrmissing data.
3053+
*
3054+
* Currently we don't seem to need to remove the attrmissing dependency in any
3055+
* other cases, but keep this as a separate function to simplify fixing things
3056+
* if that changes.
3057+
*/
3058+
static HeapTuple
3059+
MaterializeTupleForTrigger(TupleTableSlot *slot, bool *shouldFree)
3060+
{
3061+
HeapTuple tup;
3062+
TupleDesc tupdesc = slot->tts_tupleDescriptor;
3063+
3064+
tup = ExecFetchSlotHeapTuple(slot, true, shouldFree);
3065+
if (HeapTupleHeaderGetNatts(tup->t_data) < tupdesc->natts &&
3066+
tupdesc->constr && tupdesc->constr->missing)
3067+
{
3068+
HeapTuple newtup;
3069+
3070+
newtup = heap_expand_tuple(tup, tupdesc);
3071+
if (*shouldFree)
3072+
heap_freetuple(tup);
3073+
*shouldFree = true;
3074+
tup = newtup;
3075+
}
3076+
return tup;
3077+
}
3078+
30403079
/*
30413080
* Is trigger enabled to fire?
30423081
*/

src/test/regress/expected/triggers.out

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,38 @@ select * from trigtest;
214214
----+----
215215
(0 rows)
216216

217+
drop table trigtest;
218+
-- Check behavior with an implicit column default, too (bug #16644)
219+
create table trigtest (a integer);
220+
create trigger trigger_return_old
221+
before insert or delete or update on trigtest
222+
for each row execute procedure trigger_return_old();
223+
insert into trigtest values(1);
224+
select * from trigtest;
225+
a
226+
---
227+
1
228+
(1 row)
229+
230+
alter table trigtest add column b integer default 42 not null;
231+
select * from trigtest;
232+
a | b
233+
---+----
234+
1 | 42
235+
(1 row)
236+
237+
update trigtest set a = 2 where a = 1 returning *;
238+
a | b
239+
---+----
240+
1 | 42
241+
(1 row)
242+
243+
select * from trigtest;
244+
a | b
245+
---+----
246+
1 | 42
247+
(1 row)
248+
217249
drop table trigtest;
218250
create sequence ttdummy_seq increment 10 start 0 minvalue 0;
219251
create table tttest (

src/test/regress/sql/triggers.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,24 @@ select * from trigtest;
154154

155155
drop table trigtest;
156156

157+
-- Check behavior with an implicit column default, too (bug #16644)
158+
create table trigtest (a integer);
159+
160+
create trigger trigger_return_old
161+
before insert or delete or update on trigtest
162+
for each row execute procedure trigger_return_old();
163+
164+
insert into trigtest values(1);
165+
select * from trigtest;
166+
167+
alter table trigtest add column b integer default 42 not null;
168+
169+
select * from trigtest;
170+
update trigtest set a = 2 where a = 1 returning *;
171+
select * from trigtest;
172+
173+
drop table trigtest;
174+
157175
create sequence ttdummy_seq increment 10 start 0 minvalue 0;
158176

159177
create table tttest (

0 commit comments

Comments
 (0)