Skip to content

Commit 487e986

Browse files
committed
Enable BEFORE row-level triggers for partitioned tables
... with the limitation that the tuple must remain in the same partition. Reviewed-by: Ashutosh Bapat Discussion: https://postgr.es/m/20200227165158.GA2071@alvherre.pgsql
1 parent b029395 commit 487e986

File tree

8 files changed

+161
-23
lines changed

8 files changed

+161
-23
lines changed

doc/src/sgml/ref/create_trigger.sgml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
526526
Creating a row-level trigger on a partitioned table will cause identical
527527
triggers to be created in all its existing partitions; and any partitions
528528
created or attached later will contain an identical trigger, too.
529-
Triggers on partitioned tables may only be <literal>AFTER</literal>.
529+
Triggers on partitioned tables may not be <literal>INSTEAD OF</literal>.
530530
</para>
531531

532532
<para>

doc/src/sgml/trigger.sgml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@
116116
operated on, while row-level <literal>AFTER</literal> triggers fire at the end of
117117
the statement (but before any statement-level <literal>AFTER</literal> triggers).
118118
These types of triggers may only be defined on tables and
119-
foreign tables, not views; <literal>BEFORE</literal> row-level triggers may not
120-
be defined on partitioned tables.
119+
foreign tables, not views.
121120
<literal>INSTEAD OF</literal> triggers may only be
122121
defined on views, and only at row level; they fire immediately as each
123122
row in the view is identified as needing to be operated on.

src/backend/commands/tablecmds.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16546,7 +16546,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
1654616546
/*
1654716547
* Complain if we find an unexpected trigger type.
1654816548
*/
16549-
if (!TRIGGER_FOR_AFTER(trigForm->tgtype))
16549+
if (!TRIGGER_FOR_BEFORE(trigForm->tgtype) &&
16550+
!TRIGGER_FOR_AFTER(trigForm->tgtype))
1655016551
elog(ERROR, "unexpected trigger \"%s\" found",
1655116552
NameStr(trigForm->tgname));
1655216553

src/backend/commands/trigger.c

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,6 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
221221
*/
222222
if (stmt->row)
223223
{
224-
/*
225-
* BEFORE triggers FOR EACH ROW are forbidden, because they would
226-
* allow the user to direct the row to another partition, which
227-
* isn't implemented in the executor.
228-
*/
229-
if (stmt->timing != TRIGGER_TYPE_AFTER)
230-
ereport(ERROR,
231-
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
232-
errmsg("\"%s\" is a partitioned table",
233-
RelationGetRelationName(rel)),
234-
errdetail("Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.")));
235-
236224
/*
237225
* Disallow use of transition tables.
238226
*
@@ -1658,6 +1646,7 @@ RelationBuildTriggers(Relation relation)
16581646
build->tgtype = pg_trigger->tgtype;
16591647
build->tgenabled = pg_trigger->tgenabled;
16601648
build->tgisinternal = pg_trigger->tgisinternal;
1649+
build->tgisclone = OidIsValid(pg_trigger->tgparentid);
16611650
build->tgconstrrelid = pg_trigger->tgconstrrelid;
16621651
build->tgconstrindid = pg_trigger->tgconstrindid;
16631652
build->tgconstraint = pg_trigger->tgconstraint;
@@ -1961,6 +1950,8 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2)
19611950
return false;
19621951
if (trig1->tgisinternal != trig2->tgisinternal)
19631952
return false;
1953+
if (trig1->tgisclone != trig2->tgisclone)
1954+
return false;
19641955
if (trig1->tgconstrrelid != trig2->tgconstrrelid)
19651956
return false;
19661957
if (trig1->tgconstrindid != trig2->tgconstrindid)
@@ -2247,6 +2238,21 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
22472238
{
22482239
ExecForceStoreHeapTuple(newtuple, slot, false);
22492240

2241+
/*
2242+
* After a tuple in a partition goes through a trigger, the user
2243+
* could have changed the partition key enough that the tuple
2244+
* no longer fits the partition. Verify that.
2245+
*/
2246+
if (trigger->tgisclone &&
2247+
!ExecPartitionCheck(relinfo, slot, estate, false))
2248+
ereport(ERROR,
2249+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2250+
errmsg("moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported"),
2251+
errdetail("Before executing trigger \"%s\", the row was to be in partition \"%s.%s\".",
2252+
trigger->tgname,
2253+
get_namespace_name(RelationGetNamespace(relinfo->ri_RelationDesc)),
2254+
RelationGetRelationName(relinfo->ri_RelationDesc))));
2255+
22502256
if (should_free)
22512257
heap_freetuple(oldtuple);
22522258

@@ -2741,6 +2747,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
27412747
{
27422748
ExecForceStoreHeapTuple(newtuple, newslot, false);
27432749

2750+
if (trigger->tgisclone &&
2751+
!ExecPartitionCheck(relinfo, newslot, estate, false))
2752+
ereport(ERROR,
2753+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2754+
errmsg("moving row to another partition during a BEFORE trigger is not supported"),
2755+
errdetail("Before executing trigger \"%s\", the row was to be in partition \"%s.%s\".",
2756+
trigger->tgname,
2757+
get_namespace_name(RelationGetNamespace(relinfo->ri_RelationDesc)),
2758+
RelationGetRelationName(relinfo->ri_RelationDesc))));
2759+
27442760
/*
27452761
* If the tuple returned by the trigger / being stored, is the old
27462762
* row version, and the heap tuple passed to the trigger was

src/backend/partitioning/partdesc.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ CreatePartitionDirectory(MemoryContext mcxt)
307307
*
308308
* The purpose of this function is to ensure that we get the same
309309
* PartitionDesc for each relation every time we look it up. In the
310-
* face of current DDL, different PartitionDescs may be constructed with
310+
* face of concurrent DDL, different PartitionDescs may be constructed with
311311
* different views of the catalog state, but any single particular OID
312312
* will always get the same PartitionDesc for as long as the same
313313
* PartitionDirectory is used.

src/include/utils/reltrigger.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ typedef struct Trigger
2929
int16 tgtype;
3030
char tgenabled;
3131
bool tgisinternal;
32+
bool tgisclone;
3233
Oid tgconstrrelid;
3334
Oid tgconstrindid;
3435
Oid tgconstraint;

src/test/regress/expected/triggers.out

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,10 +1958,6 @@ drop table my_table;
19581958
create table parted_trig (a int) partition by list (a);
19591959
create function trigger_nothing() returns trigger
19601960
language plpgsql as $$ begin end; $$;
1961-
create trigger failed before insert or update or delete on parted_trig
1962-
for each row execute procedure trigger_nothing();
1963-
ERROR: "parted_trig" is a partitioned table
1964-
DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.
19651961
create trigger failed instead of update on parted_trig
19661962
for each row execute procedure trigger_nothing();
19671963
ERROR: "parted_trig" is a table
@@ -2246,6 +2242,80 @@ NOTICE: aasvogel <- woof!
22462242
NOTICE: trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel)
22472243
NOTICE: trigger parted_trig_odd on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel)
22482244
drop table parted_irreg_ancestor;
2245+
-- Before triggers and partitions
2246+
create table parted (a int, b int, c text) partition by list (a);
2247+
create table parted_1 partition of parted for values in (1)
2248+
partition by list (b);
2249+
create table parted_1_1 partition of parted_1 for values in (1);
2250+
create function parted_trigfunc() returns trigger language plpgsql as $$
2251+
begin
2252+
new.a = new.a + 1;
2253+
return new;
2254+
end;
2255+
$$;
2256+
insert into parted values (1, 1, 'uno uno v1'); -- works
2257+
create trigger t before insert or update or delete on parted
2258+
for each row execute function parted_trigfunc();
2259+
insert into parted values (1, 1, 'uno uno v2'); -- fail
2260+
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
2261+
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
2262+
update parted set c = c || 'v3'; -- fail
2263+
ERROR: moving row to another partition during a BEFORE trigger is not supported
2264+
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
2265+
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
2266+
begin
2267+
new.b = new.b + 1;
2268+
return new;
2269+
end;
2270+
$$;
2271+
insert into parted values (1, 1, 'uno uno v4'); -- fail
2272+
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
2273+
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
2274+
update parted set c = c || 'v5'; -- fail
2275+
ERROR: moving row to another partition during a BEFORE trigger is not supported
2276+
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
2277+
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
2278+
begin
2279+
new.c = new.c || ' and so';
2280+
return new;
2281+
end;
2282+
$$;
2283+
insert into parted values (1, 1, 'uno uno'); -- works
2284+
update parted set c = c || ' v6'; -- works
2285+
select tableoid::regclass, * from parted;
2286+
tableoid | a | b | c
2287+
------------+---+---+--------------------------
2288+
parted_1_1 | 1 | 1 | uno uno v1 v6 and so
2289+
parted_1_1 | 1 | 1 | uno uno and so v6 and so
2290+
(2 rows)
2291+
2292+
drop table parted;
2293+
create table parted (a int, b int, c text) partition by list ((a + b));
2294+
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
2295+
begin
2296+
new.a = new.a + new.b;
2297+
return new;
2298+
end;
2299+
$$;
2300+
create table parted_1 partition of parted for values in (1, 2);
2301+
create table parted_2 partition of parted for values in (3, 4);
2302+
create trigger t before insert or update on parted
2303+
for each row execute function parted_trigfunc();
2304+
insert into parted values (0, 1, 'zero win');
2305+
insert into parted values (1, 1, 'one fail');
2306+
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
2307+
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1".
2308+
insert into parted values (1, 2, 'two fail');
2309+
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
2310+
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_2".
2311+
select * from parted;
2312+
a | b | c
2313+
---+---+----------
2314+
1 | 1 | zero win
2315+
(1 row)
2316+
2317+
drop table parted;
2318+
drop function parted_trigfunc();
22492319
--
22502320
-- Constraint triggers and partitioned tables
22512321
create table parted_constr_ancestor (a int, b text)

src/test/regress/sql/triggers.sql

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,8 +1348,6 @@ drop table my_table;
13481348
create table parted_trig (a int) partition by list (a);
13491349
create function trigger_nothing() returns trigger
13501350
language plpgsql as $$ begin end; $$;
1351-
create trigger failed before insert or update or delete on parted_trig
1352-
for each row execute procedure trigger_nothing();
13531351
create trigger failed instead of update on parted_trig
13541352
for each row execute procedure trigger_nothing();
13551353
create trigger failed after update on parted_trig
@@ -1561,6 +1559,59 @@ insert into parted1_irreg values ('aardwolf', 2);
15611559
insert into parted_irreg_ancestor values ('aasvogel', 3);
15621560
drop table parted_irreg_ancestor;
15631561

1562+
-- Before triggers and partitions
1563+
create table parted (a int, b int, c text) partition by list (a);
1564+
create table parted_1 partition of parted for values in (1)
1565+
partition by list (b);
1566+
create table parted_1_1 partition of parted_1 for values in (1);
1567+
create function parted_trigfunc() returns trigger language plpgsql as $$
1568+
begin
1569+
new.a = new.a + 1;
1570+
return new;
1571+
end;
1572+
$$;
1573+
insert into parted values (1, 1, 'uno uno v1'); -- works
1574+
create trigger t before insert or update or delete on parted
1575+
for each row execute function parted_trigfunc();
1576+
insert into parted values (1, 1, 'uno uno v2'); -- fail
1577+
update parted set c = c || 'v3'; -- fail
1578+
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
1579+
begin
1580+
new.b = new.b + 1;
1581+
return new;
1582+
end;
1583+
$$;
1584+
insert into parted values (1, 1, 'uno uno v4'); -- fail
1585+
update parted set c = c || 'v5'; -- fail
1586+
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
1587+
begin
1588+
new.c = new.c || ' and so';
1589+
return new;
1590+
end;
1591+
$$;
1592+
insert into parted values (1, 1, 'uno uno'); -- works
1593+
update parted set c = c || ' v6'; -- works
1594+
select tableoid::regclass, * from parted;
1595+
1596+
drop table parted;
1597+
create table parted (a int, b int, c text) partition by list ((a + b));
1598+
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
1599+
begin
1600+
new.a = new.a + new.b;
1601+
return new;
1602+
end;
1603+
$$;
1604+
create table parted_1 partition of parted for values in (1, 2);
1605+
create table parted_2 partition of parted for values in (3, 4);
1606+
create trigger t before insert or update on parted
1607+
for each row execute function parted_trigfunc();
1608+
insert into parted values (0, 1, 'zero win');
1609+
insert into parted values (1, 1, 'one fail');
1610+
insert into parted values (1, 2, 'two fail');
1611+
select * from parted;
1612+
drop table parted;
1613+
drop function parted_trigfunc();
1614+
15641615
--
15651616
-- Constraint triggers and partitioned tables
15661617
create table parted_constr_ancestor (a int, b text)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy