Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1jQGsY-0007lM-Fg for pgsql-hackers@arkaria.postgresql.org; Sun, 19 Apr 2020 20:49:26 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1jQGsX-0006IY-3S for pgsql-hackers@arkaria.postgresql.org; Sun, 19 Apr 2020 20:49:25 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1jQGsW-0006IR-MG for pgsql-hackers@lists.postgresql.org; Sun, 19 Apr 2020 20:49:24 +0000 Received: from mail-qk1-x742.google.com ([2607:f8b0:4864:20::742]) by magus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1jQGsO-0005CW-Vs for pgsql-hackers@lists.postgresql.org; Sun, 19 Apr 2020 20:49:24 +0000 Received: by mail-qk1-x742.google.com with SMTP id n143so1763314qkn.8 for ; Sun, 19 Apr 2020 13:49:16 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=telsasoft-com.20150623.gappssmtp.com; s=20150623; h=date:from:to:cc:subject:message-id:references:mime-version :content-disposition:in-reply-to:user-agent; bh=oiHoHVmzGSX7/pNR2GP5lCAtgBolLj29oDGjxooOFlE=; b=abOjdjzmNNMRK+uZDlm85uR+x2GI6hjUxJVkmTkcKNXsdFMLILJdvvIB/KSEqmptnU 4jjAZquZGnnZvbNtwdBwU6GjrOGdm5GE2zoauHtpLFyI7e6idOOQXjOZpZtuu7YpO5Gx giu3MDfRYxdcxXOnS9JlBw/zJ6vT2c4cRWcHYE0TX4R+jK2n+0vdC0D5mUCkebxINE20 M2WpM571ygiVioIdRAawocJfD9P+xiPzVbiVXXVMlmiYqBZckyntcaOJXn6BuLLBaU66 4oI9Zc+3K6xWGoc7o3b7EfLU5ChARSW4BIvT0US374gJpsPS+n2QH05Gx6dgmwvjjMm/ 30Pw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:date:from:to:cc:subject:message-id:references :mime-version:content-disposition:in-reply-to:user-agent; bh=oiHoHVmzGSX7/pNR2GP5lCAtgBolLj29oDGjxooOFlE=; b=jKSvNJhU4Tzcd9VbXfZKV1zyQ5HHlZqsdC3/cpjPGIG2UT51N1PdzDgBFJuCUlGqAz YnVkOeG0yS7jElrSdIj9Ijikuds86lbnAC2CCgtu39KlX4aUk9xkC2nDZqBRsYpwLR4Y /EOCXbmxOyZpxgbNm9PI9772k/BwwI5VLKxqrAle1cgr/18nPlLOEoE39JG786aEtzUr kB82KSsh+R0DRG8xXE5ypVMOxd2oCzS27px3Wu3uXEGHqolOVc7ahnVh4ghhEEnMw8Lm YTa5Mz9VgcB2ZVHIWmcIxwnLP7R6TpCnMcnIg5kyv81iMlyIid8qmv8I8bLNBSbh43ro NOuQ== X-Gm-Message-State: AGi0PuaQyz2rwCHOgnu1mkTOtNeGAcGFgJEdez9KvxgPOezUbeiU9sfi yuLYLmBJjnN9tptERZwN9iTNPA== X-Google-Smtp-Source: APiQypKRFcq50As3uomKpA7dFD5aUtufTthk6H1E5i7rbJ9oLhemC53lyzUQ7wsa6Pt8ZIaZdocAOQ== X-Received: by 2002:a37:a490:: with SMTP id n138mr12699381qke.106.1587329355062; Sun, 19 Apr 2020 13:49:15 -0700 (PDT) Received: from pryzbyj (charmander.telsasoft.com. [50.244.222.1]) by smtp.gmail.com with ESMTPSA id r51sm19329755qtk.89.2020.04.19.13.49.13 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Sun, 19 Apr 2020 13:49:13 -0700 (PDT) Received: by pryzbyj (Postfix, from userid 1000) id 33C6E801644; Sun, 19 Apr 2020 15:49:12 -0500 (CDT) Date: Sun, 19 Apr 2020 15:49:12 -0500 From: Justin Pryzby To: Alvaro Herrera Cc: Tom Lane , Amit Langote , pgsql-hackers@lists.postgresql.org Subject: Re: DETACH PARTITION and FOR EACH ROW triggers on partitioned tables Message-ID: <20200419204911.GQ26953@telsasoft.com> References: <20200419002206.GM26953@telsasoft.com> <20200419191329.GA32288@alvherre.pgsql> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="Ls2Gy6y7jbHLe9Od" Content-Disposition: inline In-Reply-To: <20200419191329.GA32288@alvherre.pgsql> User-Agent: Mutt/1.9.4 (2018-02-28) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Precedence: bulk --Ls2Gy6y7jbHLe9Od Content-Type: text/plain; charset=us-ascii Content-Disposition: inline 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 --Ls2Gy6y7jbHLe9Od Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v4-0001-fix-detaching-tables-with-inherited-row-triggers.patch" From b45a047f37d215cac2e72661b92e0ade4730b1e1 Mon Sep 17 00:00:00 2001 From: Justin Pryzby 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 column_name1 [, column_name2INSTEAD OF. 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 --Ls2Gy6y7jbHLe9Od--