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
This commit is contained in:
Alvaro Herrera 2020-03-18 18:58:05 -03:00
parent b029395f5e
commit 487e9861d0
No known key found for this signature in database
GPG Key ID: 1C20ACB9D5C564AE
8 changed files with 161 additions and 23 deletions

View File

@ -526,7 +526,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
Creating a row-level trigger on a partitioned table will cause identical Creating a row-level trigger on a partitioned table will cause identical
triggers to be created in all its existing partitions; and any partitions triggers to be created in all its existing partitions; and any partitions
created or attached later will contain an identical trigger, too. created or attached later will contain an identical trigger, too.
Triggers on partitioned tables may only be <literal>AFTER</literal>. Triggers on partitioned tables may not be <literal>INSTEAD OF</literal>.
</para> </para>
<para> <para>

View File

@ -116,8 +116,7 @@
operated on, while row-level <literal>AFTER</literal> triggers fire at the end of operated on, while row-level <literal>AFTER</literal> triggers fire at the end of
the statement (but before any statement-level <literal>AFTER</literal> triggers). the statement (but before any statement-level <literal>AFTER</literal> triggers).
These types of triggers may only be defined on tables and These types of triggers may only be defined on tables and
foreign tables, not views; <literal>BEFORE</literal> row-level triggers may not foreign tables, not views.
be defined on partitioned tables.
<literal>INSTEAD OF</literal> triggers may only be <literal>INSTEAD OF</literal> triggers may only be
defined on views, and only at row level; they fire immediately as each defined on views, and only at row level; they fire immediately as each
row in the view is identified as needing to be operated on. row in the view is identified as needing to be operated on.

View File

@ -16546,7 +16546,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
/* /*
* Complain if we find an unexpected trigger type. * Complain if we find an unexpected trigger type.
*/ */
if (!TRIGGER_FOR_AFTER(trigForm->tgtype)) if (!TRIGGER_FOR_BEFORE(trigForm->tgtype) &&
!TRIGGER_FOR_AFTER(trigForm->tgtype))
elog(ERROR, "unexpected trigger \"%s\" found", elog(ERROR, "unexpected trigger \"%s\" found",
NameStr(trigForm->tgname)); NameStr(trigForm->tgname));

View File

@ -221,18 +221,6 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
*/ */
if (stmt->row) if (stmt->row)
{ {
/*
* BEFORE triggers FOR EACH ROW are forbidden, because they would
* allow the user to direct the row to another partition, which
* isn't implemented in the executor.
*/
if (stmt->timing != TRIGGER_TYPE_AFTER)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is a partitioned table",
RelationGetRelationName(rel)),
errdetail("Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.")));
/* /*
* Disallow use of transition tables. * Disallow use of transition tables.
* *
@ -1658,6 +1646,7 @@ RelationBuildTriggers(Relation relation)
build->tgtype = pg_trigger->tgtype; build->tgtype = pg_trigger->tgtype;
build->tgenabled = pg_trigger->tgenabled; build->tgenabled = pg_trigger->tgenabled;
build->tgisinternal = pg_trigger->tgisinternal; build->tgisinternal = pg_trigger->tgisinternal;
build->tgisclone = OidIsValid(pg_trigger->tgparentid);
build->tgconstrrelid = pg_trigger->tgconstrrelid; build->tgconstrrelid = pg_trigger->tgconstrrelid;
build->tgconstrindid = pg_trigger->tgconstrindid; build->tgconstrindid = pg_trigger->tgconstrindid;
build->tgconstraint = pg_trigger->tgconstraint; build->tgconstraint = pg_trigger->tgconstraint;
@ -1961,6 +1950,8 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2)
return false; return false;
if (trig1->tgisinternal != trig2->tgisinternal) if (trig1->tgisinternal != trig2->tgisinternal)
return false; return false;
if (trig1->tgisclone != trig2->tgisclone)
return false;
if (trig1->tgconstrrelid != trig2->tgconstrrelid) if (trig1->tgconstrrelid != trig2->tgconstrrelid)
return false; return false;
if (trig1->tgconstrindid != trig2->tgconstrindid) if (trig1->tgconstrindid != trig2->tgconstrindid)
@ -2247,6 +2238,21 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
{ {
ExecForceStoreHeapTuple(newtuple, slot, false); ExecForceStoreHeapTuple(newtuple, slot, false);
/*
* After a tuple in a partition goes through a trigger, the user
* could have changed the partition key enough that the tuple
* no longer fits the partition. Verify that.
*/
if (trigger->tgisclone &&
!ExecPartitionCheck(relinfo, slot, estate, false))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported"),
errdetail("Before executing trigger \"%s\", the row was to be in partition \"%s.%s\".",
trigger->tgname,
get_namespace_name(RelationGetNamespace(relinfo->ri_RelationDesc)),
RelationGetRelationName(relinfo->ri_RelationDesc))));
if (should_free) if (should_free)
heap_freetuple(oldtuple); heap_freetuple(oldtuple);
@ -2741,6 +2747,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
{ {
ExecForceStoreHeapTuple(newtuple, newslot, false); ExecForceStoreHeapTuple(newtuple, newslot, false);
if (trigger->tgisclone &&
!ExecPartitionCheck(relinfo, newslot, estate, false))
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("moving row to another partition during a BEFORE trigger is not supported"),
errdetail("Before executing trigger \"%s\", the row was to be in partition \"%s.%s\".",
trigger->tgname,
get_namespace_name(RelationGetNamespace(relinfo->ri_RelationDesc)),
RelationGetRelationName(relinfo->ri_RelationDesc))));
/* /*
* If the tuple returned by the trigger / being stored, is the old * If the tuple returned by the trigger / being stored, is the old
* row version, and the heap tuple passed to the trigger was * row version, and the heap tuple passed to the trigger was

View File

@ -307,7 +307,7 @@ CreatePartitionDirectory(MemoryContext mcxt)
* *
* The purpose of this function is to ensure that we get the same * The purpose of this function is to ensure that we get the same
* PartitionDesc for each relation every time we look it up. In the * PartitionDesc for each relation every time we look it up. In the
* face of current DDL, different PartitionDescs may be constructed with * face of concurrent DDL, different PartitionDescs may be constructed with
* different views of the catalog state, but any single particular OID * different views of the catalog state, but any single particular OID
* will always get the same PartitionDesc for as long as the same * will always get the same PartitionDesc for as long as the same
* PartitionDirectory is used. * PartitionDirectory is used.

View File

@ -29,6 +29,7 @@ typedef struct Trigger
int16 tgtype; int16 tgtype;
char tgenabled; char tgenabled;
bool tgisinternal; bool tgisinternal;
bool tgisclone;
Oid tgconstrrelid; Oid tgconstrrelid;
Oid tgconstrindid; Oid tgconstrindid;
Oid tgconstraint; Oid tgconstraint;

View File

@ -1958,10 +1958,6 @@ drop table my_table;
create table parted_trig (a int) partition by list (a); create table parted_trig (a int) partition by list (a);
create function trigger_nothing() returns trigger create function trigger_nothing() returns trigger
language plpgsql as $$ begin end; $$; language plpgsql as $$ begin end; $$;
create trigger failed before insert or update or delete on parted_trig
for each row execute procedure trigger_nothing();
ERROR: "parted_trig" is a partitioned table
DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.
create trigger failed instead of update on parted_trig create trigger failed instead of update on parted_trig
for each row execute procedure trigger_nothing(); for each row execute procedure trigger_nothing();
ERROR: "parted_trig" is a table ERROR: "parted_trig" is a table
@ -2246,6 +2242,80 @@ NOTICE: aasvogel <- woof!
NOTICE: trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel) NOTICE: trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel)
NOTICE: trigger parted_trig_odd on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel) NOTICE: trigger parted_trig_odd on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel)
drop table parted_irreg_ancestor; drop table parted_irreg_ancestor;
-- Before triggers and partitions
create table parted (a int, b int, c text) partition by list (a);
create table parted_1 partition of parted for values in (1)
partition by list (b);
create table parted_1_1 partition of parted_1 for values in (1);
create function parted_trigfunc() returns trigger language plpgsql as $$
begin
new.a = new.a + 1;
return new;
end;
$$;
insert into parted values (1, 1, 'uno uno v1'); -- works
create trigger t before insert or update or delete on parted
for each row execute function parted_trigfunc();
insert into parted values (1, 1, 'uno uno v2'); -- fail
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
update parted set c = c || 'v3'; -- fail
ERROR: moving row to another partition during a BEFORE trigger is not supported
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
begin
new.b = new.b + 1;
return new;
end;
$$;
insert into parted values (1, 1, 'uno uno v4'); -- fail
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
update parted set c = c || 'v5'; -- fail
ERROR: moving row to another partition during a BEFORE trigger is not supported
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1_1".
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
begin
new.c = new.c || ' and so';
return new;
end;
$$;
insert into parted values (1, 1, 'uno uno'); -- works
update parted set c = c || ' v6'; -- works
select tableoid::regclass, * from parted;
tableoid | a | b | c
------------+---+---+--------------------------
parted_1_1 | 1 | 1 | uno uno v1 v6 and so
parted_1_1 | 1 | 1 | uno uno and so v6 and so
(2 rows)
drop table parted;
create table parted (a int, b int, c text) partition by list ((a + b));
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
begin
new.a = new.a + new.b;
return new;
end;
$$;
create table parted_1 partition of parted for values in (1, 2);
create table parted_2 partition of parted for values in (3, 4);
create trigger t before insert or update on parted
for each row execute function parted_trigfunc();
insert into parted values (0, 1, 'zero win');
insert into parted values (1, 1, 'one fail');
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_1".
insert into parted values (1, 2, 'two fail');
ERROR: moving row to another partition during a BEFORE FOR EACH ROW trigger is not supported
DETAIL: Before executing trigger "t", the row was to be in partition "public.parted_2".
select * from parted;
a | b | c
---+---+----------
1 | 1 | zero win
(1 row)
drop table parted;
drop function parted_trigfunc();
-- --
-- Constraint triggers and partitioned tables -- Constraint triggers and partitioned tables
create table parted_constr_ancestor (a int, b text) create table parted_constr_ancestor (a int, b text)

View File

@ -1348,8 +1348,6 @@ drop table my_table;
create table parted_trig (a int) partition by list (a); create table parted_trig (a int) partition by list (a);
create function trigger_nothing() returns trigger create function trigger_nothing() returns trigger
language plpgsql as $$ begin end; $$; language plpgsql as $$ begin end; $$;
create trigger failed before insert or update or delete on parted_trig
for each row execute procedure trigger_nothing();
create trigger failed instead of update on parted_trig create trigger failed instead of update on parted_trig
for each row execute procedure trigger_nothing(); for each row execute procedure trigger_nothing();
create trigger failed after update on parted_trig create trigger failed after update on parted_trig
@ -1561,6 +1559,59 @@ insert into parted1_irreg values ('aardwolf', 2);
insert into parted_irreg_ancestor values ('aasvogel', 3); insert into parted_irreg_ancestor values ('aasvogel', 3);
drop table parted_irreg_ancestor; drop table parted_irreg_ancestor;
-- Before triggers and partitions
create table parted (a int, b int, c text) partition by list (a);
create table parted_1 partition of parted for values in (1)
partition by list (b);
create table parted_1_1 partition of parted_1 for values in (1);
create function parted_trigfunc() returns trigger language plpgsql as $$
begin
new.a = new.a + 1;
return new;
end;
$$;
insert into parted values (1, 1, 'uno uno v1'); -- works
create trigger t before insert or update or delete on parted
for each row execute function parted_trigfunc();
insert into parted values (1, 1, 'uno uno v2'); -- fail
update parted set c = c || 'v3'; -- fail
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
begin
new.b = new.b + 1;
return new;
end;
$$;
insert into parted values (1, 1, 'uno uno v4'); -- fail
update parted set c = c || 'v5'; -- fail
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
begin
new.c = new.c || ' and so';
return new;
end;
$$;
insert into parted values (1, 1, 'uno uno'); -- works
update parted set c = c || ' v6'; -- works
select tableoid::regclass, * from parted;
drop table parted;
create table parted (a int, b int, c text) partition by list ((a + b));
create or replace function parted_trigfunc() returns trigger language plpgsql as $$
begin
new.a = new.a + new.b;
return new;
end;
$$;
create table parted_1 partition of parted for values in (1, 2);
create table parted_2 partition of parted for values in (3, 4);
create trigger t before insert or update on parted
for each row execute function parted_trigfunc();
insert into parted values (0, 1, 'zero win');
insert into parted values (1, 1, 'one fail');
insert into parted values (1, 2, 'two fail');
select * from parted;
drop table parted;
drop function parted_trigfunc();
-- --
-- Constraint triggers and partitioned tables -- Constraint triggers and partitioned tables
create table parted_constr_ancestor (a int, b text) create table parted_constr_ancestor (a int, b text)