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 1jPxj4-0000fp-SE for pgsql-hackers@arkaria.postgresql.org; Sun, 19 Apr 2020 00:22:23 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1jPxj3-0001jZ-FR for pgsql-hackers@arkaria.postgresql.org; Sun, 19 Apr 2020 00:22:21 +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 1jPxj3-0001jR-3A for pgsql-hackers@lists.postgresql.org; Sun, 19 Apr 2020 00:22:21 +0000 Received: from mail-qv1-xf33.google.com ([2607:f8b0:4864:20::f33]) by magus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1jPxiu-0002sw-IE for pgsql-hackers@lists.postgresql.org; Sun, 19 Apr 2020 00:22:20 +0000 Received: by mail-qv1-xf33.google.com with SMTP id bu9so2905552qvb.13 for ; Sat, 18 Apr 2020 17:22:11 -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=w/1p+RxTXPjwblVQwS2KtwqNseu6jb0NorDg3TIWyB8=; b=f8F1G63FifDlsRoXrzDC45DTsCEk2Kb5SlI2ERR1BtnxCGdL5k2+HfJxtRa9a5qXO0 jTDgRhFMTzuJbLa6STLvfUKaMW8/Qas8c5c8q9l3lj956JFHPHBNMN1kOn1viFqSk5tz Xs2k8iyURNeyEdiT+yUEzcSeWI0wTtV/sw1vjnWObDSdwxt7zNYJ7A04ocQBiOvH1nbG n/kRLjPITXg3Kb9XSFZd1uKJEyxVoBAiHQrTXXS156465duh4oekI3yo1l9DgOuSTTgQ oa4NUQ63uD7w5Hohm7RtyAWtQLUssaDuWwEfWPv2ZbQ3/9zSvA4n7OSVj7cHWwNaRkUE 3Feg== 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=w/1p+RxTXPjwblVQwS2KtwqNseu6jb0NorDg3TIWyB8=; b=dCE7jpBGcMKCAR3z2mcjrJQaWrEBNaGh97vyN0ImLeRqq1UWgIUwl1DbN4AEOLa9lL DtD5W8J1AJFZiNdCQTOOkCw4HaB386fLd+eSSZl4hE+v1jbbNTxfJGZ/lfdfrR0R9ek8 +8XCBVtz1d4/UrFiJK+/1Ip75UhssGihGEiiVU5WL4WtF7JlpXz4juv6pNkAIBtCEqAa sW9M9ts8KiUyQ2QcF0z49SPeKWqNWBV2nkvG3geCrpj15GWNYDUIij95KVZQrkU2Sobv 25G+yqwCOeXg6XXi/ThxNV6ampWG3Ek4l9XyYXfpbCqL+zzgK5OALDcQluuiKurgVlMm V7pA== X-Gm-Message-State: AGi0PuYyJyQdH9EcsjbcLOlKwc3I5dXfTmoUdi0gP9ddbR+8G6jF/XTJ yzUCVgFd+x6FVd2DOKlET8vaAQ== X-Google-Smtp-Source: APiQypLjSHhMefL+bmzCNHSOAAOXxJWuFgSvtmu+rFyeXnO5Dcf9CFUctnyZm+79I2aI96o81CofYg== X-Received: by 2002:a0c:da87:: with SMTP id z7mr9072815qvj.141.1587255729601; Sat, 18 Apr 2020 17:22:09 -0700 (PDT) Received: from pryzbyj (charmander.telsasoft.com. [50.244.222.1]) by smtp.gmail.com with ESMTPSA id m1sm21734131qtm.22.2020.04.18.17.22.07 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Sat, 18 Apr 2020 17:22:08 -0700 (PDT) Received: by pryzbyj (Postfix, from userid 1000) id B23EF801644; Sat, 18 Apr 2020 19:22:06 -0500 (CDT) Date: Sat, 18 Apr 2020 19:22:06 -0500 From: Justin Pryzby To: Tom Lane Cc: Amit Langote , Alvaro Herrera , pgsql-hackers@lists.postgresql.org Subject: Re: DETACH PARTITION and FOR EACH ROW triggers on partitioned tables Message-ID: <20200419002206.GM26953@telsasoft.com> References: <20200408180110.GA17093@alvherre.pgsql> <5954.1586369358@sss.pgh.pa.us> <27752.1586439998@sss.pgh.pa.us> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="sm4nu43k4a2Rpi4c" Content-Disposition: inline In-Reply-To: <27752.1586439998@sss.pgh.pa.us> User-Agent: Mutt/1.9.4 (2018-02-28) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Precedence: bulk --sm4nu43k4a2Rpi4c Content-Type: text/plain; charset=us-ascii Content-Disposition: inline On Thu, Apr 09, 2020 at 09:46:38AM -0400, Tom Lane wrote: > Amit Langote writes: > > On Thu, Apr 9, 2020 at 3:09 AM Tom Lane 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 --sm4nu43k4a2Rpi4c Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v2-0001-fix-detaching-tables-with-inherited-after-row-tri.patch" From 19fe0c70e0b4f2c05c538d1a9042f9303c927ae2 Mon Sep 17 00:00:00 2001 From: Justin Pryzby 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 column_name1 [, column_name2INSTEAD OF. 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 --sm4nu43k4a2Rpi4c--