DETACH PARTITION and FOR EACH ROW triggers on partitioned tables
This seems to be a bug in master, v12, and (probably) v11, where "FOR EACH FOR"
was first allowed on partition tables (86f575948).
I thought this would work like partitioned indexes (8b08f7d48), where detaching
a partition makes its index non-inherited, and attaching a partition marks a
pre-existing, matching partition as inherited rather than creating a new one.
DROP TABLE t, t1;
CREATE TABLE t(i int)PARTITION BY RANGE(i);
CREATE TABLE t1 PARTITION OF t FOR VALUES FROM(1)TO(2);
CREATE OR REPLACE FUNCTION trigf() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN END $$;
CREATE TRIGGER trig AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION trigf();
SELECT tgrelid::regclass, * FROM pg_trigger WHERE tgrelid='t1'::regclass;
ALTER TABLE t DETACH PARTITION t1;
ALTER TABLE t ATTACH PARTITION t1 FOR VALUES FROM (1)TO(2);
ERROR: trigger "trig" for relation "t1" already exists
DROP TRIGGER trig ON t1;
ERROR: cannot drop trigger trig on table t1 because trigger trig on table t requires it
HINT: You can drop trigger trig on table t instead.
I remember these, but they don't seem to be relevant to this issue, which seems
to be independant.
1fa846f1c9 Fix cloning of row triggers to sub-partitions
b9b408c487 Record parents of triggers
The commit for partitioned indexes talks about using an pre-existing index on
the child as a "convenience gadget", puts indexes into pg_inherit, and
introduces "ALTER INDEX..ATTACH PARTITION" and "CREATE INDEX..ON ONLY".
It's probably rare for a duplicate index to be useful (unless rebuilding to be
more optimal, which is probably not reasonably interspersed with altering
inheritence). But I don't know if that's equally true for triggers. So I'm
not sure what the intended behavior is, so I've stopped after implementing
a partial fix.
Attachments:
0001-WIP-fix-detaching-tables-with-inherited-triggers.patchtext/x-diff; charset=us-asciiDownload
From 2c31cac22178d904ee108b77f316886d1e2f6288 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Fri, 3 Apr 2020 22:43:26 -0500
Subject: [PATCH] WIP: fix detaching tables with inherited triggers
---
src/backend/commands/tablecmds.c | 33 ++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 037d457c3d..10a60e158f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16797,6 +16797,39 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
}
table_close(classRel, RowExclusiveLock);
+ /* detach triggers too */
+ {
+ /* XXX: relcache.c */
+ ScanKeyData skey;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+ Relation tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partRel)));
+
+ scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
+ true, NULL, 1, &skey);
+
+ while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
+ {
+ Form_pg_trigger pg_trigger;
+ trigtup = heap_copytuple(trigtup); /* need a modifiable copy */
+ pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
+ /* Set the trigger's parent to Invalid */
+ if (!OidIsValid(pg_trigger->tgparentid))
+ continue;
+ if (!pg_trigger->tgisinternal)
+ continue;
+ pg_trigger->tgparentid = InvalidOid;
+ pg_trigger->tgisinternal = false;
+ CatalogTupleUpdate(tgrel, &trigtup->t_self, trigtup);
+ heap_freetuple(trigtup);
+ }
+ systable_endscan(scan);
+ table_close(tgrel, RowExclusiveLock);
+ }
+
/*
* Detach any foreign keys that are inherited. This includes creating
* additional action triggers.
--
2.17.0
On 2020-Apr-08, Justin Pryzby wrote:
This seems to be a bug in master, v12, and (probably) v11, where "FOR EACH FOR"
was first allowed on partition tables (86f575948).I thought this would work like partitioned indexes (8b08f7d48), where detaching
a partition makes its index non-inherited, and attaching a partition marks a
pre-existing, matching partition as inherited rather than creating a new one.
Hmm. Let's agree to what behavior we want, and then we implement that.
It seems to me there are two choices:
1. on detach, keep the trigger but make it independent of the trigger on
parent. (This requires that the trigger is made dependent on the
trigger on parent, if the table is attached as partition again;
otherwise you'd end up with multiple copies of the trigger if you
detach/attach multiple times).
2. on detach, remove the trigger from the partition.
I think (2) is easier to implement, but (1) is the more convenient
behavior.
(The current behavior is obviously a bug.)
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Wed, Apr 08, 2020 at 12:02:39PM -0400, Alvaro Herrera wrote:
On 2020-Apr-08, Justin Pryzby wrote:
This seems to be a bug in master, v12, and (probably) v11, where "FOR EACH FOR"
was first allowed on partition tables (86f575948).I thought this would work like partitioned indexes (8b08f7d48), where detaching
a partition makes its index non-inherited, and attaching a partition marks a
pre-existing, matching partition as inherited rather than creating a new one.Hmm. Let's agree to what behavior we want, and then we implement that.
It seems to me there are two choices:1. on detach, keep the trigger but make it independent of the trigger on
parent. (This requires that the trigger is made dependent on the
trigger on parent, if the table is attached as partition again;
otherwise you'd end up with multiple copies of the trigger if you
detach/attach multiple times).2. on detach, remove the trigger from the partition.
I think (2) is easier to implement, but (1) is the more convenient
behavior.
At telsasoft, we don't care (we uninherit tables before ALTERing parents to
avoid disruptive locking and to avoid worst-case disk use).
(1) is consistent with the behavior for indexes, which is a slight advantage
for users' ability to understand and keep track of the behavior. But adding
triggers is pretty different so I'm not sure it's a totally compelling
parallel.
--
Justin
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
Hmm. Let's agree to what behavior we want, and then we implement that.
It seems to me there are two choices:
1. on detach, keep the trigger but make it independent of the trigger on
parent. (This requires that the trigger is made dependent on the
trigger on parent, if the table is attached as partition again;
otherwise you'd end up with multiple copies of the trigger if you
detach/attach multiple times).
2. on detach, remove the trigger from the partition.
I think (2) is easier to implement, but (1) is the more convenient
behavior.
I think that #1 would soon lead to needing all the same infrastructure
as we have for inherited columns and constraints, ie triggers would need
equivalents of attislocal and attinhcount. I don't really want to go
there, so I'd vote for #2.
regards, tom lane
On 2020-Apr-08, Tom Lane wrote:
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
Hmm. Let's agree to what behavior we want, and then we implement that.
It seems to me there are two choices:1. on detach, keep the trigger but make it independent of the trigger on
parent. (This requires that the trigger is made dependent on the
trigger on parent, if the table is attached as partition again;
otherwise you'd end up with multiple copies of the trigger if you
detach/attach multiple times).2. on detach, remove the trigger from the partition.
I think (2) is easier to implement, but (1) is the more convenient
behavior.I think that #1 would soon lead to needing all the same infrastructure
as we have for inherited columns and constraints, ie triggers would need
equivalents of attislocal and attinhcount. I don't really want to go
there, so I'd vote for #2.
Hmm. Those things are used for the legacy inheritance case supporting
multiple inheritance, where we need to figure out which parent the table
is being detached (disinherited) from. But for partitioning we know
which parent it is, since there can only be one. So I don't think that
argument applies.
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
On 2020-Apr-08, Tom Lane wrote:
I think that #1 would soon lead to needing all the same infrastructure
as we have for inherited columns and constraints, ie triggers would need
equivalents of attislocal and attinhcount. I don't really want to go
there, so I'd vote for #2.
Hmm. Those things are used for the legacy inheritance case supporting
multiple inheritance, where we need to figure out which parent the table
is being detached (disinherited) from. But for partitioning we know
which parent it is, since there can only be one. So I don't think that
argument applies.
My point is that so long as you only allow the case of exactly one parent,
you can just delete the child trigger, because it must belong to that
parent. As soon as there's any flexibility, you are going to end up
reinventing all the stuff we had to invent to manage
maybe-or-maybe-not-inherited columns. So I think the "detach" idea
is the first step on that road, and I counsel not taking that step.
(This implies that when creating a child trigger, we should error out,
*not* allow the case, if there's already a trigger by that name. Not
sure if that's what happens today, but again I'd say that's what we
should do to avoid complicated cases.)
regards, tom lane
On Thu, Apr 9, 2020 at 3:09 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Alvaro Herrera <alvherre@2ndquadrant.com> writes:
On 2020-Apr-08, Tom Lane wrote:
I think that #1 would soon lead to needing all the same infrastructure
as we have for inherited columns and constraints, ie triggers would need
equivalents of attislocal and attinhcount. I don't really want to go
there, so I'd vote for #2.Hmm. Those things are used for the legacy inheritance case supporting
multiple inheritance, where we need to figure out which parent the table
is being detached (disinherited) from. But for partitioning we know
which parent it is, since there can only be one. So I don't think that
argument applies.My point is that so long as you only allow the case of exactly one parent,
you can just delete the child trigger, because it must belong to that
parent. As soon as there's any flexibility, you are going to end up
reinventing all the stuff we had to invent to manage
maybe-or-maybe-not-inherited columns. So I think the "detach" idea
is the first step on that road, and I counsel not taking that step.
As mentioned upthread, we have behavior #1 for indexes (attach
existing / detach & keep), without any of the *islocal, *inhcount
infrastructure. It is a bit complex, because we need logic to check
the equivalence of an existing index on the partition being attached,
so implementing the same behavior for trigger is going to have to be
almost as complex. Considering that #2 will be much simpler to
implement, but would be asymmetric with everything else.
--
Amit Langote
EnterpriseDB: http://www.enterprisedb.com
Amit Langote <amitlangote09@gmail.com> writes:
On Thu, Apr 9, 2020 at 3:09 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
My point is that so long as you only allow the case of exactly one parent,
you can just delete the child trigger, because it must belong to that
parent. As soon as there's any flexibility, you are going to end up
reinventing all the stuff we had to invent to manage
maybe-or-maybe-not-inherited columns. So I think the "detach" idea
is the first step on that road, and I counsel not taking that step.
As mentioned upthread, we have behavior #1 for indexes (attach
existing / detach & keep), without any of the *islocal, *inhcount
infrastructure. It is a bit complex, because we need logic to check
the equivalence of an existing index on the partition being attached,
so implementing the same behavior for trigger is going to have to be
almost as complex. Considering that #2 will be much simpler to
implement, but would be asymmetric with everything else.
I think there is justification for jumping through some hoops for
indexes, because they can be extremely expensive to recreate.
The same argument doesn't hold even a little bit for child
triggers, though.
Also it can be expected that an index will still behave sensibly after
its table is standalone, whereas that's far from obvious for a trigger
that was meant to work on partition member tables.
regards, tom lane
On Thu, Apr 09, 2020 at 09:46:38AM -0400, Tom Lane wrote:
Amit Langote <amitlangote09@gmail.com> writes:
On Thu, Apr 9, 2020 at 3:09 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
My point is that so long as you only allow the case of exactly one parent,
you can just delete the child trigger, because it must belong to that
parent. As soon as there's any flexibility, you are going to end up
reinventing all the stuff we had to invent to manage
maybe-or-maybe-not-inherited columns. So I think the "detach" idea
is the first step on that road, and I counsel not taking that step.As mentioned upthread, we have behavior #1 for indexes (attach
existing / detach & keep), without any of the *islocal, *inhcount
infrastructure. It is a bit complex, because we need logic to check
the equivalence of an existing index on the partition being attached,
so implementing the same behavior for trigger is going to have to be
almost as complex. Considering that #2 will be much simpler to
implement, but would be asymmetric with everything else.I think there is justification for jumping through some hoops for
indexes, because they can be extremely expensive to recreate.
The same argument doesn't hold even a little bit for child
triggers, though.Also it can be expected that an index will still behave sensibly after
its table is standalone, whereas that's far from obvious for a trigger
that was meant to work on partition member tables.
I haven't heard a compelling argument for or against either way.
Maybe the worst behavior might be if, during ATTACH, we searched for a matching
trigger, and "merged" it (marked it inherited) if it matched. That could be
bad if someone *wanted* two triggers, which seems unlikely, but to each their
own.
I implemented the simple way (and, as an experiment, 75% of the hard way).
It occured to me that we don't currently distinguish between a trigger on a
child table, and a trigger on a parent table which was recursively created on a
child. That makes sense for indexes and constraints, since the objects persist
if the table is detached, so it doesn't matter how it was defined.
But if we remove trigger during DETACH, then it's *not* the same as a trigger
that was defined on the child, and I suggest that in v13 we should make that
visible.
--
Justin
Attachments:
v2-0001-fix-detaching-tables-with-inherited-after-row-tri.patchtext/x-diff; charset=us-asciiDownload
From 19fe0c70e0b4f2c05c538d1a9042f9303c927ae2 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Fri, 3 Apr 2020 22:43:26 -0500
Subject: [PATCH v2] fix detaching tables with inherited after-row triggers
The old behavior is buggy, and the intended behavior was not previously
documented. So define the behavior that the trigger is removed if the table is
detached. It is an error if a table being attached already has a trigger of
the same. This differs from the behavior for indexes and constraints.
See also:
86f575948c773b0ec5b0f27066e37dd93a7f0a96
Discussion:
https://www.postgresql.org/message-id/flat/20200408152412.GZ2228%40telsasoft.com
---
doc/src/sgml/ref/create_trigger.sgml | 1 +
src/backend/commands/tablecmds.c | 37 ++++++++++++++++++++++
src/bin/psql/describe.c | 12 +++++--
src/test/regress/expected/triggers.out | 44 ++++++++++++++++++++++++++
src/test/regress/sql/triggers.sql | 18 +++++++++++
5 files changed, 110 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index bde3a63f90..303881fcfb 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -526,6 +526,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
Creating a row-level trigger on a partitioned table will cause identical
triggers to be created in all its existing partitions; and any partitions
created or attached later will contain an identical trigger, too.
+ If the partition is detached from its parent, the trigger is removed.
Triggers on partitioned tables may not be <literal>INSTEAD OF</literal>.
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 037d457c3d..78236d152f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16797,6 +16797,43 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
}
table_close(classRel, RowExclusiveLock);
+ /*
+ * Drop any ROW triggers inherited from partitioned table.
+ * This undoes what CloneRowTriggersToPartition did.
+ */
+ {
+ ScanKeyData skey;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+ Relation tgrel;
+
+ ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partRel)));
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+ scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
+ true, NULL, 1, &skey);
+
+ while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
+ {
+ Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (OidIsValid(pg_trigger->tgparentid) &&
+ pg_trigger->tgisinternal &&
+ TRIGGER_FOR_ROW(pg_trigger->tgtype))
+ RemoveTriggerById(pg_trigger->oid);
+
+ deleteDependencyRecordsFor(TriggerRelationId,
+ pg_trigger->oid,
+ false);
+ deleteDependencyRecordsFor(RelationRelationId,
+ pg_trigger->oid,
+ false);
+ }
+
+ systable_endscan(scan);
+ table_close(tgrel, RowExclusiveLock);
+ }
+
/*
* Detach any foreign keys that are inherited. This includes creating
* additional action triggers.
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f05e914b4d..4cbc741c97 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2939,14 +2939,17 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf,
"SELECT t.tgname, "
"pg_catalog.pg_get_triggerdef(t.oid%s), "
- "t.tgenabled, %s\n"
+ "t.tgenabled, %s, %s\n"
"FROM pg_catalog.pg_trigger t\n"
"WHERE t.tgrelid = '%s' AND ",
(pset.sversion >= 90000 ? ", true" : ""),
(pset.sversion >= 90000 ? "t.tgisinternal" :
pset.sversion >= 80300 ?
"t.tgconstraint <> 0 AS tgisinternal" :
- "false AS tgisinternal"), oid);
+ "false AS tgisinternal"),
+ (pset.sversion >= 13000 ? "t.tgparentid" :
+ "0 AS tgparentid"),
+ oid);
if (pset.sversion >= 110000)
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D') \n"
" OR EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
@@ -3062,6 +3065,11 @@ describeOneTableDetails(const char *schemaname,
tgdef = usingpos + 9;
printfPQExpBuffer(&buf, " %s", tgdef);
+
+ /* Visually distinguish inherited triggers XXX: ROW only? */
+ if (*PQgetvalue(result, i, 4) != '0')
+ appendPQExpBufferStr(&buf, ", PARTITION");
+
printTableAddFooter(&cont, buf.data);
}
}
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e9da4ef983..9b544148bf 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2023,6 +2023,50 @@ select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
---------+--------+--------
(0 rows)
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), PARTITION
+
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+ERROR: trigger "trg1" for table "trigpart3" does not exist
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+drop table trigpart3;
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname~'^trg1' order by 1;
+ tgrelid | tgname | tgfoid | tgenabled | tgisinternal
+------------+--------+-----------------+-----------+--------------
+ trigpart | trg1 | trigger_nothing | O | f
+ trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart4 | trg1 | trigger_nothing | O | t
+ trigpart41 | trg1 | trigger_nothing | O | t
+ trigpart42 | trg1 | trigger_nothing | O | t
+(5 rows)
+
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000)to(3000); -- fail
+ERROR: trigger "trg1" for relation "trigpart3" already exists
+drop table trigpart3;
drop table trigpart;
drop function trigger_nothing();
--
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 80ffbb4b02..5ab7cc62d2 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1380,6 +1380,24 @@ drop trigger trg1 on trigpart; -- ok, all gone
select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+drop table trigpart3;
+
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname~'^trg1' order by 1;
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000)to(3000); -- fail
+drop table trigpart3;
+
drop table trigpart;
drop function trigger_nothing();
--
2.17.0
v3 fixes a brown-paper-bag logic error.
--
Justin
Attachments:
v3-0001-fix-detaching-tables-with-inherited-row-triggers.patchtext/x-diff; charset=us-asciiDownload
From b5de1fc71f805bb8c7ec7e77bfce9a604ccd4bc8 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Fri, 3 Apr 2020 22:43:26 -0500
Subject: [PATCH v3] fix detaching tables with inherited row triggers
The old behavior is buggy, and the intended behavior was not previously
documented. So define the behavior that the trigger is removed if the table is
detached. It is an error if a table being attached already has a trigger of
the same. This differs from the behavior for indexes and constraints.
See also:
86f575948c773b0ec5b0f27066e37dd93a7f0a96
Discussion:
https://www.postgresql.org/message-id/flat/20200408152412.GZ2228%40telsasoft.com
---
doc/src/sgml/ref/create_trigger.sgml | 1 +
src/backend/commands/tablecmds.c | 38 ++++++++++++++++++++++
src/bin/psql/describe.c | 12 +++++--
src/test/regress/expected/triggers.out | 44 ++++++++++++++++++++++++++
src/test/regress/sql/triggers.sql | 18 +++++++++++
5 files changed, 111 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index bde3a63f90..303881fcfb 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -526,6 +526,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
Creating a row-level trigger on a partitioned table will cause identical
triggers to be created in all its existing partitions; and any partitions
created or attached later will contain an identical trigger, too.
+ If the partition is detached from its parent, the trigger is removed.
Triggers on partitioned tables may not be <literal>INSTEAD OF</literal>.
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 037d457c3d..480ea777ac 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16797,6 +16797,44 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
}
table_close(classRel, RowExclusiveLock);
+ /*
+ * Drop any ROW triggers inherited from partitioned table.
+ * This undoes what CloneRowTriggersToPartition did.
+ */
+ {
+ ScanKeyData skey;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+ Relation tgrel;
+
+ ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partRel)));
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+ scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
+ true, NULL, 1, &skey);
+
+ while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
+ {
+ Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (!OidIsValid(pg_trigger->tgparentid) ||
+ !pg_trigger->tgisinternal ||
+ !TRIGGER_FOR_ROW(pg_trigger->tgtype))
+ continue;
+
+ RemoveTriggerById(pg_trigger->oid);
+ deleteDependencyRecordsFor(TriggerRelationId,
+ pg_trigger->oid,
+ false);
+ deleteDependencyRecordsFor(RelationRelationId,
+ pg_trigger->oid,
+ false);
+ }
+
+ systable_endscan(scan);
+ table_close(tgrel, RowExclusiveLock);
+ }
+
/*
* Detach any foreign keys that are inherited. This includes creating
* additional action triggers.
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f05e914b4d..4cbc741c97 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2939,14 +2939,17 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf,
"SELECT t.tgname, "
"pg_catalog.pg_get_triggerdef(t.oid%s), "
- "t.tgenabled, %s\n"
+ "t.tgenabled, %s, %s\n"
"FROM pg_catalog.pg_trigger t\n"
"WHERE t.tgrelid = '%s' AND ",
(pset.sversion >= 90000 ? ", true" : ""),
(pset.sversion >= 90000 ? "t.tgisinternal" :
pset.sversion >= 80300 ?
"t.tgconstraint <> 0 AS tgisinternal" :
- "false AS tgisinternal"), oid);
+ "false AS tgisinternal"),
+ (pset.sversion >= 13000 ? "t.tgparentid" :
+ "0 AS tgparentid"),
+ oid);
if (pset.sversion >= 110000)
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D') \n"
" OR EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
@@ -3062,6 +3065,11 @@ describeOneTableDetails(const char *schemaname,
tgdef = usingpos + 9;
printfPQExpBuffer(&buf, " %s", tgdef);
+
+ /* Visually distinguish inherited triggers XXX: ROW only? */
+ if (*PQgetvalue(result, i, 4) != '0')
+ appendPQExpBufferStr(&buf, ", PARTITION");
+
printTableAddFooter(&cont, buf.data);
}
}
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e9da4ef983..9b544148bf 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2023,6 +2023,50 @@ select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
---------+--------+--------
(0 rows)
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), PARTITION
+
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+ERROR: trigger "trg1" for table "trigpart3" does not exist
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+drop table trigpart3;
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname~'^trg1' order by 1;
+ tgrelid | tgname | tgfoid | tgenabled | tgisinternal
+------------+--------+-----------------+-----------+--------------
+ trigpart | trg1 | trigger_nothing | O | f
+ trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart4 | trg1 | trigger_nothing | O | t
+ trigpart41 | trg1 | trigger_nothing | O | t
+ trigpart42 | trg1 | trigger_nothing | O | t
+(5 rows)
+
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000)to(3000); -- fail
+ERROR: trigger "trg1" for relation "trigpart3" already exists
+drop table trigpart3;
drop table trigpart;
drop function trigger_nothing();
--
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 80ffbb4b02..5ab7cc62d2 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1380,6 +1380,24 @@ drop trigger trg1 on trigpart; -- ok, all gone
select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+drop table trigpart3;
+
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname~'^trg1' order by 1;
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000)to(3000); -- fail
+drop table trigpart3;
+
drop table trigpart;
drop function trigger_nothing();
--
2.17.0
On 2020-Apr-18, Justin Pryzby wrote:
I haven't heard a compelling argument for or against either way.
Maybe the worst behavior might be if, during ATTACH, we searched for a matching
trigger, and "merged" it (marked it inherited) if it matched. That could be
bad if someone *wanted* two triggers, which seems unlikely, but to each their
own.
I think the simplicity argument trumps the other ones, so I agree to go
with your v3 patch proposed downthread.
What happens if you detach the parent? I mean, should the trigger
removal recurse to children?
It occured to me that we don't currently distinguish between a trigger on a
child table, and a trigger on a parent table which was recursively created on a
child. That makes sense for indexes and constraints, since the objects persist
if the table is detached, so it doesn't matter how it was defined.But if we remove trigger during DETACH, then it's *not* the same as a trigger
that was defined on the child, and I suggest that in v13 we should make that
visible.
Hmm, interesting point -- whether the trigger is partition or not is
important because it affects what happens on detach. I agree that we
should make it visible. Is the proposed single bit "PARTITION" good
enough, or should we indicate what's the ancestor table that defines the
partition?
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Wed, Apr 08, 2020 at 11:44:33AM -0500, Justin Pryzby wrote:
On Wed, Apr 08, 2020 at 12:02:39PM -0400, Alvaro Herrera wrote:
On 2020-Apr-08, Justin Pryzby wrote:
This seems to be a bug in master, v12, and (probably) v11, where "FOR EACH FOR"
was first allowed on partition tables (86f575948).I thought this would work like partitioned indexes (8b08f7d48), where detaching
a partition makes its index non-inherited, and attaching a partition marks a
pre-existing, matching partition as inherited rather than creating a new one.Hmm. Let's agree to what behavior we want, and then we implement that.
It seems to me there are two choices:1. on detach, keep the trigger but make it independent of the trigger on
parent. (This requires that the trigger is made dependent on the
trigger on parent, if the table is attached as partition again;
otherwise you'd end up with multiple copies of the trigger if you
detach/attach multiple times).2. on detach, remove the trigger from the partition.
I think (2) is easier to implement, but (1) is the more convenient
behavior.At telsasoft, we don't care (we uninherit tables before ALTERing parents to
avoid disruptive locking and to avoid worst-case disk use).
I realized that I was wrong about what would be most desirable for us, for an
uncommon case:
Our loader INSERTs into the child table, not the parent (I think I did that to
try to implement UPSERT before partitioned indexes in v11).
All but the newest partitions are DETACHed when we need to promote a column.
It's probably rare that we'd be inserting into a table old enough to be
detached, and normally that would be ok, but if a trigger were missing, it
would misbehave. In our use-case, we're creating trigger on the parent as a
convenient way to maintain them on the partitions, which doesn't work if a
table exists but detached..
So we'd actually prefer the behavior of indexes/constraints, where the trigger
is preserved if the child is detached. I'm not requesting to do that just for
our use case, which may be atypical or not a good model, but adding our one
data point.
--
Justin
On 2020-Apr-19, Justin Pryzby wrote:
It's probably rare that we'd be inserting into a table old enough to be
detached, and normally that would be ok, but if a trigger were missing, it
would misbehave. In our use-case, we're creating trigger on the parent as a
convenient way to maintain them on the partitions, which doesn't work if a
table exists but detached..So we'd actually prefer the behavior of indexes/constraints, where the trigger
is preserved if the child is detached. I'm not requesting to do that just for
our use case, which may be atypical or not a good model, but adding our one
data point.
I think the easiest way to implement this is to have two triggers -- the
one that's direct in the partition checks whether the table is a
partition and does nothing in that case.
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Sun, Apr 19, 2020 at 03:13:29PM -0400, Alvaro Herrera wrote:
On 2020-Apr-18, Justin Pryzby wrote:
I haven't heard a compelling argument for or against either way.
Maybe the worst behavior might be if, during ATTACH, we searched for a matching
trigger, and "merged" it (marked it inherited) if it matched. That could be
bad if someone *wanted* two triggers, which seems unlikely, but to each their
own.I think the simplicity argument trumps the other ones, so I agree to go
with your v3 patch proposed downthread.What happens if you detach the parent? I mean, should the trigger
removal recurse to children?
It think it should probably exactly undo what CloneRowTriggersToPartition does.
..and I guess you're trying to politely say that it didn't. I tried to fix in
v4 - please check if that's right.
It occured to me that we don't currently distinguish between a trigger on a
child table, and a trigger on a parent table which was recursively created on a
child. That makes sense for indexes and constraints, since the objects persist
if the table is detached, so it doesn't matter how it was defined.But if we remove trigger during DETACH, then it's *not* the same as a trigger
that was defined on the child, and I suggest that in v13 we should make that
visible.Hmm, interesting point -- whether the trigger is partition or not is
important because it affects what happens on detach. I agree that we
should make it visible. Is the proposed single bit "PARTITION" good
enough, or should we indicate what's the ancestor table that defines the
partition?
Yea, it's an obvious thing to do.
One issue is that tgparentid is new, so showing the partition status of the
trigger when connected to an pre-13 server would be nontrivial: b9b408c48.
--
Justin
Attachments:
v4-0001-fix-detaching-tables-with-inherited-row-triggers.patchtext/x-diff; charset=us-asciiDownload
From b45a047f37d215cac2e72661b92e0ade4730b1e1 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Fri, 3 Apr 2020 22:43:26 -0500
Subject: [PATCH v4] fix detaching tables with inherited row triggers
The old behavior is buggy, and the intended behavior was not previously
documented. So define the behavior that the trigger is removed if the table is
detached. It is an error if a table being attached already has a trigger of
the same. This differs from the behavior for indexes and constraints.
See also:
86f575948c773b0ec5b0f27066e37dd93a7f0a96
Discussion:
https://www.postgresql.org/message-id/flat/20200408152412.GZ2228%40telsasoft.com
---
doc/src/sgml/ref/create_trigger.sgml | 1 +
src/backend/commands/tablecmds.c | 42 ++++++++++++++++++++++++
src/bin/psql/describe.c | 14 ++++++--
src/test/regress/expected/triggers.out | 45 ++++++++++++++++++++++++++
src/test/regress/sql/triggers.sql | 21 ++++++++++++
5 files changed, 121 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index bde3a63f90..303881fcfb 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -526,6 +526,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
Creating a row-level trigger on a partitioned table will cause identical
triggers to be created in all its existing partitions; and any partitions
created or attached later will contain an identical trigger, too.
+ If the partition is detached from its parent, the trigger is removed.
Triggers on partitioned tables may not be <literal>INSTEAD OF</literal>.
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 037d457c3d..514c5ff844 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16797,6 +16797,48 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
}
table_close(classRel, RowExclusiveLock);
+ /*
+ * Drop any ROW triggers inherited from partitioned table.
+ * This undoes what CloneRowTriggersToPartition did.
+ */
+ {
+ ScanKeyData skey;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+ Relation tgrel;
+
+ ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partRel)));
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+ scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
+ true, NULL, 1, &skey);
+
+ while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
+ {
+ Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress object;
+
+ if (!OidIsValid(pg_trigger->tgparentid) ||
+ !pg_trigger->tgisinternal ||
+ !TRIGGER_FOR_ROW(pg_trigger->tgtype))
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId,
+ pg_trigger->oid,
+ false);
+ deleteDependencyRecordsFor(RelationRelationId,
+ pg_trigger->oid,
+ false);
+
+ CommandCounterIncrement();
+ ObjectAddressSet(object, TriggerRelationId, pg_trigger->oid);
+ performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+ }
+
+ systable_endscan(scan);
+ table_close(tgrel, RowExclusiveLock);
+ }
+
/*
* Detach any foreign keys that are inherited. This includes creating
* additional action triggers.
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f05e914b4d..1de33ac772 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2939,14 +2939,18 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf,
"SELECT t.tgname, "
"pg_catalog.pg_get_triggerdef(t.oid%s), "
- "t.tgenabled, %s\n"
+ "t.tgenabled, %s, %s\n"
"FROM pg_catalog.pg_trigger t\n"
"WHERE t.tgrelid = '%s' AND ",
(pset.sversion >= 90000 ? ", true" : ""),
(pset.sversion >= 90000 ? "t.tgisinternal" :
pset.sversion >= 80300 ?
"t.tgconstraint <> 0 AS tgisinternal" :
- "false AS tgisinternal"), oid);
+ "false AS tgisinternal"),
+ (pset.sversion >= 13000 ?
+ "pg_partition_root(t.tgrelid) AS parent" :
+ "'' AS parent"),
+ oid);
if (pset.sversion >= 110000)
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D') \n"
" OR EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
@@ -3062,6 +3066,12 @@ describeOneTableDetails(const char *schemaname,
tgdef = usingpos + 9;
printfPQExpBuffer(&buf, " %s", tgdef);
+
+ /* Visually distinguish inherited triggers XXX: ROW only? */
+ if (!PQgetisnull(result, i, 4))
+ appendPQExpBuffer(&buf, ", ON TABLE %s",
+ PQgetvalue(result, i, 4));
+
printTableAddFooter(&cont, buf.data);
}
}
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e9da4ef983..a760eb15da 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2023,6 +2023,51 @@ select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
---------+--------+--------
(0 rows)
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), ON TABLE trigpart
+
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+ERROR: trigger "trg1" for table "trigpart3" does not exist
+alter table trigpart detach partition trigpart4;
+drop trigger trg1 on trigpart41; -- fail due to "does not exist"
+ERROR: trigger "trg1" for table "trigpart41" does not exist
+drop table trigpart4;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+drop table trigpart3;
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname~'^trg1' order by 1;
+ tgrelid | tgname | tgfoid | tgenabled | tgisinternal
+-----------+--------+-----------------+-----------+--------------
+ trigpart | trg1 | trigger_nothing | O | f
+ trigpart1 | trg1 | trigger_nothing | O | t
+(2 rows)
+
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000)to(3000); -- fail
+ERROR: trigger "trg1" for relation "trigpart3" already exists
+drop table trigpart3;
drop table trigpart;
drop function trigger_nothing();
--
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 80ffbb4b02..4b250fcc8d 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1380,6 +1380,27 @@ drop trigger trg1 on trigpart; -- ok, all gone
select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+alter table trigpart detach partition trigpart4;
+drop trigger trg1 on trigpart41; -- fail due to "does not exist"
+drop table trigpart4;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+drop table trigpart3;
+
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname~'^trg1' order by 1;
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000)to(3000); -- fail
+drop table trigpart3;
+
drop table trigpart;
drop function trigger_nothing();
--
2.17.0
On Mon, Apr 20, 2020 at 5:49 AM Justin Pryzby <pryzby@telsasoft.com> wrote:
On Sun, Apr 19, 2020 at 03:13:29PM -0400, Alvaro Herrera wrote:
On 2020-Apr-18, Justin Pryzby wrote:
I haven't heard a compelling argument for or against either way.
Maybe the worst behavior might be if, during ATTACH, we searched for a matching
trigger, and "merged" it (marked it inherited) if it matched. That could be
bad if someone *wanted* two triggers, which seems unlikely, but to each their
own.I think the simplicity argument trumps the other ones, so I agree to go
with your v3 patch proposed downthread.What happens if you detach the parent? I mean, should the trigger
removal recurse to children?It think it should probably exactly undo what CloneRowTriggersToPartition does.
..and I guess you're trying to politely say that it didn't. I tried to fix in
v4 - please check if that's right.
Looks correct to me. Maybe have a test cover that?
It occured to me that we don't currently distinguish between a trigger on a
child table, and a trigger on a parent table which was recursively created on a
child. That makes sense for indexes and constraints, since the objects persist
if the table is detached, so it doesn't matter how it was defined.But if we remove trigger during DETACH, then it's *not* the same as a trigger
that was defined on the child, and I suggest that in v13 we should make that
visible.Hmm, interesting point -- whether the trigger is partition or not is
important because it affects what happens on detach. I agree that we
should make it visible. Is the proposed single bit "PARTITION" good
enough, or should we indicate what's the ancestor table that defines the
partition?Yea, it's an obvious thing to do.
This:
+ "false AS tgisinternal"),
+ (pset.sversion >= 13000 ?
+ "pg_partition_root(t.tgrelid) AS parent" :
+ "'' AS parent"),
+ oid);
looks wrong, because the actual partition root may not also be the
trigger parent root, for example:
create table f (a int references p) partition by list (a);
create table f1 partition of f for values in (1) partition by list (a);
create table f11 partition of f for values in (1);
create function trigfunc() returns trigger language plpgsql as $$
begin raise notice '%', new; return new; end; $$;
create trigger trig before insert on f1 for each row execute function
trigfunc();
\d f11
Table "public.f11"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | |
Partition of: f1 FOR VALUES IN (1)
Triggers:
trig BEFORE INSERT ON f11 FOR EACH ROW EXECUTE FUNCTION
trigfunc(), ON TABLE f
Here, ON TABLE should say "f1".
The following gets the correct parent for me:
- (pset.sversion >= 13000 ?
- "pg_partition_root(t.tgrelid) AS parent" :
- "'' AS parent"),
+ (pset.sversion >= 130000 ?
+ "(SELECT relid"
+ " FROM pg_trigger, pg_partition_ancestors(t.tgrelid)"
+ " WHERE tgname = t.tgname AND tgrelid = relid"
+ " AND tgparentid = 0) AS parent" :
+ " null AS parent"),
The server version number being compared against was missing a zero in
your patch.
Also, how about, for consistency, making the parent table labeling of
the trigger look similar to that for the foreign constraint, so
instead of:
Triggers:
trig BEFORE INSERT ON f11 FOR EACH ROW EXECUTE FUNCTION
trigfunc(), ON TABLE f1
how about:
Triggers:
TABLE "f1" TRIGGER "trig" BEFORE INSERT ON f11 FOR EACH ROW
EXECUTE FUNCTION trigfunc()
--
Amit Langote
EnterpriseDB: http://www.enterprisedb.com
On Mon, Apr 20, 2020 at 06:35:44PM +0900, Amit Langote wrote:
On Mon, Apr 20, 2020 at 5:49 AM Justin Pryzby <pryzby@telsasoft.com> wrote:
On Sun, Apr 19, 2020 at 03:13:29PM -0400, Alvaro Herrera wrote:
What happens if you detach the parent? I mean, should the trigger
removal recurse to children?It think it should probably exactly undo what CloneRowTriggersToPartition does.
..and I guess you're trying to politely say that it didn't. I tried to fix in
v4 - please check if that's right.Looks correct to me. Maybe have a test cover that?
I included such a test with the v4 patch.
But if we remove trigger during DETACH, then it's *not* the same as a trigger
that was defined on the child, and I suggest that in v13 we should make that
visible.Hmm, interesting point -- whether the trigger is partition or not is
important because it affects what happens on detach. I agree that we
should make it visible. Is the proposed single bit "PARTITION" good
enough, or should we indicate what's the ancestor table that defines the
partition?Yea, it's an obvious thing to do.
This:
+ "false AS tgisinternal"), + (pset.sversion >= 13000 ? + "pg_partition_root(t.tgrelid) AS parent" : + "'' AS parent"), + oid);looks wrong, because the actual partition root may not also be the
trigger parent root, for example:
Sigh, right.
The following gets the correct parent for me:
- (pset.sversion >= 13000 ? - "pg_partition_root(t.tgrelid) AS parent" : - "'' AS parent"), + (pset.sversion >= 130000 ? + "(SELECT relid" + " FROM pg_trigger, pg_partition_ancestors(t.tgrelid)" + " WHERE tgname = t.tgname AND tgrelid = relid" + " AND tgparentid = 0) AS parent" : + " null AS parent"),
I'm happy to see that this doesn't require a recursive cte, at least.
I was trying to think how to break it by returning multiple results or results
out of order, but I think that can't happen.
Also, how about, for consistency, making the parent table labeling of
the trigger look similar to that for the foreign constraint, so
Triggers:
TABLE "f1" TRIGGER "trig" BEFORE INSERT ON f11 FOR EACH ROW EXECUTE FUNCTION trigfunc()
I'll leave that for committer to decide.
I split into separate patches since only 0001 should be backpatched (with
s/OidIsValid(tgparentid)/isPartitionTrigger/).
--
Justin
Attachments:
v5-0001-fix-detaching-tables-with-inherited-row-triggers.patchtext/x-diff; charset=us-asciiDownload
From dc24d8fdbe4435feaea9a99fcf3e1e1121cc8950 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Fri, 3 Apr 2020 22:43:26 -0500
Subject: [PATCH v5 1/2] fix detaching tables with inherited row triggers
The old behavior is buggy, and the intended behavior was not previously
documented. So define the behavior that the trigger is removed if the table is
detached. It is an error if a table being attached already has a trigger of
the same. This differs from the behavior for indexes and constraints.
Should backpatch to v11 with s/OidIsValid(tgparentid)/isPartitionTrigger/.
See also:
86f575948c773b0ec5b0f27066e37dd93a7f0a96
Discussion:
https://www.postgresql.org/message-id/flat/20200408152412.GZ2228%40telsasoft.com
---
doc/src/sgml/ref/create_trigger.sgml | 1 +
src/backend/commands/tablecmds.c | 42 ++++++++++++++++++++++++
src/test/regress/expected/triggers.out | 45 ++++++++++++++++++++++++++
src/test/regress/sql/triggers.sql | 21 ++++++++++++
4 files changed, 109 insertions(+)
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index bde3a63f90..303881fcfb 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -526,6 +526,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
Creating a row-level trigger on a partitioned table will cause identical
triggers to be created in all its existing partitions; and any partitions
created or attached later will contain an identical trigger, too.
+ If the partition is detached from its parent, the trigger is removed.
Triggers on partitioned tables may not be <literal>INSTEAD OF</literal>.
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 037d457c3d..514c5ff844 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16797,6 +16797,48 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
}
table_close(classRel, RowExclusiveLock);
+ /*
+ * Drop any ROW triggers inherited from partitioned table.
+ * This undoes what CloneRowTriggersToPartition did.
+ */
+ {
+ ScanKeyData skey;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+ Relation tgrel;
+
+ ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(partRel)));
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+ scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
+ true, NULL, 1, &skey);
+
+ while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
+ {
+ Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress object;
+
+ if (!OidIsValid(pg_trigger->tgparentid) ||
+ !pg_trigger->tgisinternal ||
+ !TRIGGER_FOR_ROW(pg_trigger->tgtype))
+ continue;
+
+ deleteDependencyRecordsFor(TriggerRelationId,
+ pg_trigger->oid,
+ false);
+ deleteDependencyRecordsFor(RelationRelationId,
+ pg_trigger->oid,
+ false);
+
+ CommandCounterIncrement();
+ ObjectAddressSet(object, TriggerRelationId, pg_trigger->oid);
+ performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+ }
+
+ systable_endscan(scan);
+ table_close(tgrel, RowExclusiveLock);
+ }
+
/*
* Detach any foreign keys that are inherited. This includes creating
* additional action triggers.
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e9da4ef983..0e30029bcf 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2023,6 +2023,51 @@ select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
---------+--------+--------
(0 rows)
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+ERROR: trigger "trg1" for table "trigpart3" does not exist
+alter table trigpart detach partition trigpart4;
+drop trigger trg1 on trigpart41; -- fail due to "does not exist"
+ERROR: trigger "trg1" for table "trigpart41" does not exist
+drop table trigpart4;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+drop table trigpart3;
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname~'^trg1' order by 1;
+ tgrelid | tgname | tgfoid | tgenabled | tgisinternal
+-----------+--------+-----------------+-----------+--------------
+ trigpart | trg1 | trigger_nothing | O | f
+ trigpart1 | trg1 | trigger_nothing | O | t
+(2 rows)
+
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000)to(3000); -- fail
+ERROR: trigger "trg1" for relation "trigpart3" already exists
+drop table trigpart3;
drop table trigpart;
drop function trigger_nothing();
--
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 80ffbb4b02..4b250fcc8d 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1380,6 +1380,27 @@ drop trigger trg1 on trigpart; -- ok, all gone
select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+alter table trigpart detach partition trigpart4;
+drop trigger trg1 on trigpart41; -- fail due to "does not exist"
+drop table trigpart4;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000)to(3000);
+drop table trigpart3;
+
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname~'^trg1' order by 1;
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000)to(3000); -- fail
+drop table trigpart3;
+
drop table trigpart;
drop function trigger_nothing();
--
2.17.0
v5-0002-Make-inherited-status-of-triggers-visible-in-psql.patchtext/x-diff; charset=us-asciiDownload
From 3b76f4d2546d94d0a35ec28899e431c77dc6101d Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Mon, 20 Apr 2020 13:46:06 -0500
Subject: [PATCH v5 2/2] Make inherited status of triggers visible in psql..
Unlike inherited indexes and constraints, the trigger is removed if the table
is detached. Make that visibly apparent. HEAD/v13 only.
---
src/bin/psql/describe.c | 17 +++++++++++++++--
src/test/regress/expected/triggers.out | 2 +-
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index f05e914b4d..ead4e0bf47 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2939,14 +2939,21 @@ describeOneTableDetails(const char *schemaname,
printfPQExpBuffer(&buf,
"SELECT t.tgname, "
"pg_catalog.pg_get_triggerdef(t.oid%s), "
- "t.tgenabled, %s\n"
+ "t.tgenabled, %s, %s\n"
"FROM pg_catalog.pg_trigger t\n"
"WHERE t.tgrelid = '%s' AND ",
(pset.sversion >= 90000 ? ", true" : ""),
(pset.sversion >= 90000 ? "t.tgisinternal" :
pset.sversion >= 80300 ?
"t.tgconstraint <> 0 AS tgisinternal" :
- "false AS tgisinternal"), oid);
+ "false AS tgisinternal"),
+ (pset.sversion >= 130000 ?
+ "(SELECT a.relid"
+ " FROM pg_trigger AS u, pg_partition_ancestors(t.tgrelid) AS a"
+ " WHERE u.tgname = t.tgname AND u.tgrelid = a.relid"
+ " AND u.tgparentid = 0) AS parent" :
+ "'' AS parent"),
+ oid);
if (pset.sversion >= 110000)
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D') \n"
" OR EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
@@ -3062,6 +3069,12 @@ describeOneTableDetails(const char *schemaname,
tgdef = usingpos + 9;
printfPQExpBuffer(&buf, " %s", tgdef);
+
+ /* Visually distinguish inherited triggers XXX: ROW only? */
+ if (!PQgetisnull(result, i, 4))
+ appendPQExpBuffer(&buf, ", ON TABLE %s",
+ PQgetvalue(result, i, 4));
+
printTableAddFooter(&cont, buf.data);
}
}
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 0e30029bcf..a760eb15da 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2033,7 +2033,7 @@ create trigger trg1 after insert on trigpart for each row execute procedure trig
b | integer | | |
Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
Triggers:
- trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), ON TABLE trigpart
alter table trigpart detach partition trigpart3;
drop trigger trg1 on trigpart3; -- fail due to "does not exist"
--
2.17.0
+ deleteDependencyRecordsFor(TriggerRelationId, + pg_trigger->oid, + false); + deleteDependencyRecordsFor(RelationRelationId, + pg_trigger->oid, + false); + + CommandCounterIncrement(); + ObjectAddressSet(object, TriggerRelationId, pg_trigger->oid); + performDeletion(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + } + + systable_endscan(scan); + table_close(tgrel, RowExclusiveLock); + }
Two small issues here. First, your second call to
deleteDependencyRecordsFor did nothing, because your first call deleted
all the dependency records. I changed that to two
deleteDependencyRecordsForClass() calls, that actually do what you
intended.
The other is that instead of deleting each trigger, we can accumulate
them to delete with a single performMultipleDeletions call; this also
means we get to do CommandCounterIncrement just once.
v6 fixes those things and AFAICS is ready to push.
I haven't reviewed your 0002 carefully, but (as inventor of the "TABLE
t" marker for FK constraints) I agree with Amit that we should imitate
that instead of coming up with a new way to show it.
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Attachments:
v6-0001-Fix-detaching-tables-with-inherited-row-triggers.patchtext/x-diff; charset=iso-8859-1Download
From b8aeae162e03ccd0346212e19ae2d75ec6495288 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Mon, 20 Apr 2020 18:39:59 -0400
Subject: [PATCH v6] Fix detaching tables with inherited row triggers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The old behavior is buggy, and the intended behavior was not previously
documented. So define the behavior that the trigger is removed if the table is
detached. It is an error if a table being attached already has a trigger of
the same. This differs from the behavior for indexes and constraints.
Should backpatch to v11 with s/OidIsValid(tgparentid)/isPartitionTrigger/.
See also: 86f575948c77
Author: Justin Pryzby <pryzby@telsasoft.com>
Reviewed-by: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Ãlvaro Herrera <alvherre@alvh.no-ip.org>
Discussion: https://www.postgresql.org/message-id/flat/20200408152412.GZ2228%40telsasoft.com
---
doc/src/sgml/ref/create_trigger.sgml | 1 +
src/backend/commands/tablecmds.c | 66 ++++++++++++++++++++++++++
src/test/regress/expected/triggers.out | 45 ++++++++++++++++++
src/test/regress/sql/triggers.sql | 21 ++++++++
4 files changed, 133 insertions(+)
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index bde3a63f90..303881fcfb 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -526,6 +526,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
Creating a row-level trigger on a partitioned table will cause identical
triggers to be created in all its existing partitions; and any partitions
created or attached later will contain an identical trigger, too.
+ If the partition is detached from its parent, the trigger is removed.
Triggers on partitioned tables may not be <literal>INSTEAD OF</literal>.
</para>
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 037d457c3d..3ebbd5d013 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -547,6 +547,7 @@ static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
List *partConstraint,
bool validate_default);
static void CloneRowTriggersToPartition(Relation parent, Relation partition);
+static void DropClonedTriggersFromPartition(Oid partitionId);
static ObjectAddress ATExecDetachPartition(Relation rel, RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation rel,
RangeVar *name);
@@ -16797,6 +16798,9 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
}
table_close(classRel, RowExclusiveLock);
+ /* Drop any triggers that were cloned on creation/attach. */
+ DropClonedTriggersFromPartition(RelationGetRelid(partRel));
+
/*
* Detach any foreign keys that are inherited. This includes creating
* additional action triggers.
@@ -16881,6 +16885,68 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
return address;
}
+/*
+ * DropClonedTriggersFromPartition
+ * subroutine for ATExecDetachPartition to remove any triggers that were
+ * cloned to the partition when it was created-as-partition or attached.
+ * This undoes what CloneRowTriggersToPartition did.
+ */
+static void
+DropClonedTriggersFromPartition(Oid partitionId)
+{
+ ScanKeyData skey;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+ Relation tgrel;
+ ObjectAddresses *objects;
+
+ objects = new_object_addresses();
+
+ /*
+ * Scan pg_trigger to search for all triggers on this rel.
+ */
+ ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+ F_OIDEQ, ObjectIdGetDatum(partitionId));
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+ scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
+ true, NULL, 1, &skey);
+ while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
+ {
+ Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
+ ObjectAddress trig;
+
+ /* Ignore triggers that weren't cloned */
+ if (!OidIsValid(pg_trigger->tgparentid) ||
+ !pg_trigger->tgisinternal ||
+ !TRIGGER_FOR_ROW(pg_trigger->tgtype))
+ continue;
+
+ /*
+ * This is ugly, but necessary: remove the dependency markings on the
+ * trigger so that it can be removed.
+ */
+ deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+
+ /* remember this trigger to remove it below */
+ ObjectAddressSet(trig, TriggerRelationId, pg_trigger->oid);
+ add_exact_object_address(&trig, objects);
+ }
+
+ /* make the dependency removal visible to the deletion below */
+ CommandCounterIncrement();
+ performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+
+ /* done */
+ free_object_addresses(objects);
+ systable_endscan(scan);
+ table_close(tgrel, RowExclusiveLock);
+}
+
/*
* Before acquiring lock on an index, acquire the same lock on the owning
* table.
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e9da4ef983..c1482e185b 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2023,6 +2023,51 @@ select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
---------+--------+--------
(0 rows)
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+ERROR: trigger "trg1" for table "trigpart3" does not exist
+alter table trigpart detach partition trigpart4;
+drop trigger trg1 on trigpart41; -- fail due to "does not exist"
+ERROR: trigger "trg1" for table "trigpart41" does not exist
+drop table trigpart4;
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
+drop table trigpart3;
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname ~ '^trg1' order by 1;
+ tgrelid | tgname | tgfoid | tgenabled | tgisinternal
+-----------+--------+-----------------+-----------+--------------
+ trigpart | trg1 | trigger_nothing | O | f
+ trigpart1 | trg1 | trigger_nothing | O | t
+(2 rows)
+
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+ Table "public.trigpart3"
+ Column | Type | Collation | Nullable | Default
+--------+---------+-----------+----------+---------
+ a | integer | | |
+ b | integer | | |
+Triggers:
+ trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
+ERROR: trigger "trg1" for relation "trigpart3" already exists
+drop table trigpart3;
drop table trigpart;
drop function trigger_nothing();
--
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 80ffbb4b02..e228d0a8a5 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1380,6 +1380,27 @@ drop trigger trg1 on trigpart; -- ok, all gone
select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+alter table trigpart detach partition trigpart4;
+drop trigger trg1 on trigpart41; -- fail due to "does not exist"
+drop table trigpart4;
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
+drop table trigpart3;
+
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+ where tgname ~ '^trg1' order by 1;
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
+drop table trigpart3;
+
drop table trigpart;
drop function trigger_nothing();
--
2.20.1
On 2020-Apr-20, Alvaro Herrera wrote:
+ while (HeapTupleIsValid(trigtup = systable_getnext(scan))) + { + Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup); + ObjectAddress trig; + + /* Ignore triggers that weren't cloned */ + if (!OidIsValid(pg_trigger->tgparentid) || + !pg_trigger->tgisinternal || + !TRIGGER_FOR_ROW(pg_trigger->tgtype)) + continue;
Actually, shouldn't we be checking just "!OidIsValid(pg_trigger->tgparentid)"
here? Surely the other two conditions should already not matter either
way if tgparentid is set. I can't see us starting to clone
for-statement triggers, but I'm not sure I trust the internal marking to
remain one way or the other.
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
I think I also owe the attached doc updates.
--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Attachments:
doc.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 7595e609b5..233905552c 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -869,13 +869,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
one will be created in the attached table; or, if an equivalent
index already exists, it will be attached to the target table's index,
as if <command>ALTER INDEX ATTACH PARTITION</command> had been executed.
Note that if the existing table is a foreign table, it is currently not
allowed to attach the table as a partition of the target table if there
are <literal>UNIQUE</literal> indexes on the target table. (See also
- <xref linkend="sql-createforeigntable"/>.)
+ <xref linkend="sql-createforeigntable"/>.) For each user-defined
+ row-level trigger that exists in the target table, a corresponding one
+ is created in the attached table.
</para>
<para>
A partition using <literal>FOR VALUES</literal> uses same syntax for
<replaceable class="parameter">partition_bound_spec</replaceable> as
<xref linkend="sql-createtable"/>. The partition bound specification
@@ -941,13 +943,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>DETACH PARTITION</literal> <replaceable class="parameter">partition_name</replaceable></term>
<listitem>
<para>
This form detaches specified partition of the target table. The detached
partition continues to exist as a standalone table, but no longer has any
ties to the table from which it was detached. Any indexes that were
- attached to the target table's indexes are detached.
+ attached to the target table's indexes are detached. Any triggers that
+ were created to mirror those in the target table are removed.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 155866c7c8..c49c770dd0 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -396,13 +396,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<term><literal>PARTITION OF <replaceable class="parameter">parent_table</replaceable> { FOR VALUES <replaceable class="parameter">partition_bound_spec</replaceable> | DEFAULT }</literal></term>
<listitem>
<para>
Creates the table as a <firstterm>partition</firstterm> of the specified
parent table. The table can be created either as a partition for specific
values using <literal>FOR VALUES</literal> or as a default partition
- using <literal>DEFAULT</literal>.
+ using <literal>DEFAULT</literal>. Any indexes, constraints and
+ user-defined row-level triggers that exist in the parent table are cloned
+ on the new partition.
</para>
<para>
The <replaceable class="parameter">partition_bound_spec</replaceable>
must correspond to the partitioning method and partition key of the
parent table, and must not overlap with any existing partition of that
On Tue, Apr 21, 2020 at 12:20:38PM -0400, Alvaro Herrera wrote:
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 7595e609b5..233905552c 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -941,13 +943,14 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM <term><literal>DETACH PARTITION</literal> <replaceable class="parameter">partition_name</replaceable></term> <listitem> <para> This form detaches specified partition of the target table. The detached partition continues to exist as a standalone table, but no longer has any ties to the table from which it was detached. Any indexes that were - attached to the target table's indexes are detached. + attached to the target table's indexes are detached. Any triggers that + were created to mirror those in the target table are removed.
Can you say "cloned" here instead of mirror ?
+ attached to the target table's indexes are detached. Any triggers that + were created as clones of triggers in the target table are removed.
Also, I see in the surrounding context a missing word?
This form detaches THE specified partition of the target table.
--
Justin
On 2020-Apr-20, Justin Pryzby wrote:
On Mon, Apr 20, 2020 at 06:35:44PM +0900, Amit Langote wrote:
Also, how about, for consistency, making the parent table labeling of
the trigger look similar to that for the foreign constraint, so
Triggers:
TABLE "f1" TRIGGER "trig" BEFORE INSERT ON f11 FOR EACH ROW EXECUTE FUNCTION trigfunc()I'll leave that for committer to decide.
Pushed. Many thanks for this!
Changes: I thought that printing the "ON TABLE" bit when it's defined in
the same table is pointless and ugly, so I added a NULLIF to prevent it
in that case (it's not every day that you can put NULLIF to work). I
also changed the empty string to NULL for the case with older servers,
so that it doesn't print a lame "ON TABLE " clause for them. Lastly,
added pg_catalog qualifications everywhere needed.
Contrary to what I had said, I decided to leave the output as submitted;
the constraint lines are not really precedent against it:
55432 13devel 24286=# \d lev3
Partitioned table "public.lev3"
Column │ Type │ Collation │ Nullable │ Default
────────┼─────────┼───────────┼──────────┼─────────
a │ integer │ │ not null │
Partition of: lev2 FOR VALUES IN (3)
Partition key: LIST (a)
Indexes:
"lev3_pkey" PRIMARY KEY, btree (a)
Foreign-key constraints:
TABLE "lev1" CONSTRAINT "lev1_a_fkey" FOREIGN KEY (a) REFERENCES lev1(a)
Referenced by:
TABLE "lev1" CONSTRAINT "lev1_a_fkey" FOREIGN KEY (a) REFERENCES lev1(a)
Triggers:
tt AFTER UPDATE ON lev3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), ON TABLE lev2
Number of partitions: 1 (Use \d+ to list them.)
In the "FK constraints" and "referenced by" entries, it looks natural
since the constraint refers to a table. Not so in the trigger case.
--
Álvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Tue, Apr 21, 2020 at 07:03:30PM -0400, Alvaro Herrera wrote:
On 2020-Apr-20, Justin Pryzby wrote:
On Mon, Apr 20, 2020 at 06:35:44PM +0900, Amit Langote wrote:
Also, how about, for consistency, making the parent table labeling of
the trigger look similar to that for the foreign constraint, so
Triggers:
TABLE "f1" TRIGGER "trig" BEFORE INSERT ON f11 FOR EACH ROW EXECUTE FUNCTION trigfunc()I'll leave that for committer to decide.
Pushed. Many thanks for this!
Thanks for polishing it.
I was just about to convince myself of the merits of doing it Amit's way :)
I noticed a few issues:
- should put \n's around Amit's subquery to make psql -E look pretty;
- maybe should quote the TABLE, like \"%s\" ?
#3 is that *if* we did it Amit's way, I *think* maybe we should show the
parent's triggerdef, not the childs.
It seems strange to me to say "TABLE trigpart .* INSERT ON trigpart3"
- TABLE "trigpart" TRIGGER trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+ TABLE "trigpart" TRIGGER trg1 AFTER INSERT ON trigpart FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
--
Justin
Attachments:
v6-0001-fixups-c33869cc3bfc42bce822251f2fa1a2a346f86cc5.patchtext/x-diff; charset=us-asciiDownload
From 86ff4e592afe56d2611e22b63fa3f1268b583e58 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Fri, 3 Apr 2020 22:43:26 -0500
Subject: [PATCH v6 1/2] fixups: c33869cc3bfc42bce822251f2fa1a2a346f86cc5
---
src/bin/psql/describe.c | 14 +++++++-------
src/test/regress/expected/triggers.out | 2 +-
src/test/regress/sql/triggers.sql | 2 +-
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 8dca6d8bb4..5ef56f7a9d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2947,12 +2947,12 @@ describeOneTableDetails(const char *schemaname,
pset.sversion >= 80300 ?
"t.tgconstraint <> 0 AS tgisinternal" :
"false AS tgisinternal"),
- (pset.sversion >= 130000 ?
- "(SELECT (NULLIF(a.relid, t.tgrelid))::pg_catalog.regclass"
- " FROM pg_catalog.pg_trigger AS u, "
- " pg_catalog.pg_partition_ancestors(t.tgrelid) AS a"
- " WHERE u.tgname = t.tgname AND u.tgrelid = a.relid"
- " AND u.tgparentid = 0) AS parent" :
+ (pset.sversion >= 130000 ? "\n"
+ " (SELECT (NULLIF(a.relid, t.tgrelid))::pg_catalog.regclass\n"
+ " FROM pg_catalog.pg_trigger AS u,\n"
+ " pg_catalog.pg_partition_ancestors(t.tgrelid) AS a\n"
+ " WHERE u.tgname = t.tgname AND u.tgrelid = a.relid\n"
+ " AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
if (pset.sversion >= 110000)
@@ -3073,7 +3073,7 @@ describeOneTableDetails(const char *schemaname,
/* Visually distinguish inherited triggers */
if (!PQgetisnull(result, i, 4))
- appendPQExpBuffer(&buf, ", ON TABLE %s",
+ appendPQExpBuffer(&buf, ", ON TABLE \"%s\"",
PQgetvalue(result, i, 4));
printTableAddFooter(&cont, buf.data);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5e76b3a47e..4104711c29 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2065,7 +2065,7 @@ create trigger trg1 after insert on trigpart3 for each row execute procedure tri
Triggers:
trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
-alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000); -- fail
ERROR: trigger "trg1" for relation "trigpart3" already exists
drop table trigpart3;
drop table trigpart;
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index e228d0a8a5..c359c6d3fa 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1398,7 +1398,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
create table trigpart3 (like trigpart);
create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
\d trigpart3
-alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000); -- fail
drop table trigpart3;
drop table trigpart;
--
2.17.0
v6-0002-show-inherited-triggers-Amit-s-way.patchtext/x-diff; charset=us-asciiDownload
From 4712253f66e03f6666d57a7a7a971a9f00c492c6 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Mon, 20 Apr 2020 13:46:06 -0500
Subject: [PATCH v6 2/2] show inherited triggers Amit's way..
..this also updates the query to avoid saying things like:
TABLE trigpart .* INSERT ON trigpart3
---
src/bin/psql/describe.c | 42 +++++++++++++++++---------
src/test/regress/expected/triggers.out | 2 +-
2 files changed, 28 insertions(+), 16 deletions(-)
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 5ef56f7a9d..3b02eab84e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2936,10 +2936,27 @@ describeOneTableDetails(const char *schemaname,
PGresult *result;
int tuples;
- printfPQExpBuffer(&buf,
+ if (pset.sversion >= 130000)
+ {
+ printfPQExpBuffer(&buf,
+ "SELECT t.tgname, "
+ "pg_catalog.pg_get_triggerdef(COALESCE(u.oid, t.oid), true), "
+ "t.tgenabled, t.tgisinternal, a.relid AS othertable\n"
+ "FROM pg_catalog.pg_trigger t LEFT JOIN\n"
+ "pg_catalog.pg_partition_ancestors(t.tgrelid) AS a\n"
+ "ON true LEFT JOIN\n"
+ "pg_catalog.pg_trigger AS u ON\n"
+ "u.tgname = t.tgname AND u.tgrelid = a.relid\n"
+ "WHERE t.tgrelid = '%s' AND "
+ "(u.tgparentid = 0 OR a.relid IS NULL) AND \n",
+ oid);
+ }
+ else
+ {
+ printfPQExpBuffer(&buf,
"SELECT t.tgname, "
"pg_catalog.pg_get_triggerdef(t.oid%s), "
- "t.tgenabled, %s, %s\n"
+ "t.tgenabled, %s, NULL AS parent\n"
"FROM pg_catalog.pg_trigger t\n"
"WHERE t.tgrelid = '%s' AND ",
(pset.sversion >= 90000 ? ", true" : ""),
@@ -2947,14 +2964,9 @@ describeOneTableDetails(const char *schemaname,
pset.sversion >= 80300 ?
"t.tgconstraint <> 0 AS tgisinternal" :
"false AS tgisinternal"),
- (pset.sversion >= 130000 ? "\n"
- " (SELECT (NULLIF(a.relid, t.tgrelid))::pg_catalog.regclass\n"
- " FROM pg_catalog.pg_trigger AS u,\n"
- " pg_catalog.pg_partition_ancestors(t.tgrelid) AS a\n"
- " WHERE u.tgname = t.tgname AND u.tgrelid = a.relid\n"
- " AND u.tgparentid = 0) AS parent" :
- "NULL AS parent"),
oid);
+ }
+
if (pset.sversion >= 110000)
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D') \n"
" OR EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
@@ -3069,12 +3081,12 @@ describeOneTableDetails(const char *schemaname,
if (usingpos)
tgdef = usingpos + 9;
- printfPQExpBuffer(&buf, " %s", tgdef);
-
- /* Visually distinguish inherited triggers */
- if (!PQgetisnull(result, i, 4))
- appendPQExpBuffer(&buf, ", ON TABLE \"%s\"",
- PQgetvalue(result, i, 4));
+ if (PQgetisnull(result, i, 4))
+ printfPQExpBuffer(&buf, " %s", tgdef);
+ else
+ /* Visually distinguish inherited triggers */
+ printfPQExpBuffer(&buf, " TABLE \"%s\" TRIGGER %s",
+ PQgetvalue(result, i, 4), tgdef);
printTableAddFooter(&cont, buf.data);
}
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 4104711c29..a1b81f49ff 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2033,7 +2033,7 @@ create trigger trg1 after insert on trigpart for each row execute procedure trig
b | integer | | |
Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
Triggers:
- trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing(), ON TABLE trigpart
+ TABLE "trigpart" TRIGGER trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
alter table trigpart detach partition trigpart3;
drop trigger trg1 on trigpart3; -- fail due to "does not exist"
--
2.17.0