Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.92) (envelope-from ) id 1jCn7p-0002JH-QK for pgsql-hackers@arkaria.postgresql.org; Fri, 13 Mar 2020 16:25:30 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.89) (envelope-from ) id 1jCn7o-0003gl-EW for pgsql-hackers@arkaria.postgresql.org; Fri, 13 Mar 2020 16:25:28 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_CBC_SHA1:256) (Exim 4.89) (envelope-from ) id 1jCn7n-0003gJ-Tm for pgsql-hackers@lists.postgresql.org; Fri, 13 Mar 2020 16:25:28 +0000 Received: from mail-qk1-x742.google.com ([2607:f8b0:4864:20::742]) by makus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1jCn7k-0002Rq-UU for pgsql-hackers@lists.postgresql.org; Fri, 13 Mar 2020 16:25:26 +0000 Received: by mail-qk1-x742.google.com with SMTP id z25so8583321qkj.4 for ; Fri, 13 Mar 2020 09:25:24 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=2ndquadrant-com.20150623.gappssmtp.com; s=20150623; h=date:from:to:cc:subject:message-id:mime-version:content-disposition :content-transfer-encoding:in-reply-to:user-agent; bh=xk/cc7oJ4hpSKBzIlFgjoc2vRRks8PGJnBVJu7aSLyA=; b=0mQrCt9jEr/fb9chL0bTdsthuyROEcVHhiVp882rHi+LBxJL3MKUyGiCqM74LeoeTr Zyx68OzXm/XyY2DRpBZDAW2TVuKcL3G12b0x38VogP0FSqxHC+Gw1L3VHR81UzUIQhW4 Xn84eveBW8JvuJAnupeRXAEGc/B6DybEHS04uOAEFpSHBdbnyrYobLLryu+c+2D+tBwK Mb4BgMRmwKwnRdjiYK3zJ/mAG5DCBWJ6BVMcPUH/71E1PiZqkOS3QzM4EkDYHtzCvymq K7pUYSFzwlS7BYIrfvM7BPGJkWmKk3IlXoZ41Oo9YlHlMxg3lHxIDDGMBT1T6lruzPB8 xfNg== 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:mime-version :content-disposition:content-transfer-encoding:in-reply-to :user-agent; bh=xk/cc7oJ4hpSKBzIlFgjoc2vRRks8PGJnBVJu7aSLyA=; b=PZXVTTxt92Qg4VTiTaHOi0+XiU6EosBhs41USVgMWK2N68wuqgVCONwsuQC53HLnEb nimXIRjFQUpryCOJj77VBoMl3ywbgyuxtPvBmpt7sZ8AwEB6Blk3pwHtB0zboTw6oBrq TYt/a2/dVoonTq0OZ9LvGTKUr49+HqGYxi7KJuHaiUHnz8ZcNLEntawRll1EoQvDCWvx T2E4cDu7GUNJ9sDx8uRm9ZbHhcfhm55VI0bxJ1Qxfruhqyc8Jl+gOIcfsnJiZ1WSVYJW qDSjWK3j/7yPbT0XWH6mRojMllh7/U/c1F4vZ4Ag06V0io3CYIQt6i1RuQUcFdN6UVhs stRw== X-Gm-Message-State: ANhLgQ3j5h2OMW5iPa8iO04kd1/8b4Ny818TPd9X4mxJl7nvZv9S1xL2 wad/UjFbzcI34S0TTxgvuveHTA== X-Google-Smtp-Source: ADFU+vs1buZq88WubvCBM8HBcYmzHvzGNHcyNoUyIKqVl3i7OdJ18B2iCuecx61qkLuKO3NgX0f3jw== X-Received: by 2002:a37:9f58:: with SMTP id i85mr14175283qke.186.1584116723850; Fri, 13 Mar 2020 09:25:23 -0700 (PDT) Received: from nimloth.alvh.no-ip.org ([190.121.31.1]) by smtp.gmail.com with ESMTPSA id r4sm16777918qkm.98.2020.03.13.09.25.22 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 13 Mar 2020 09:25:22 -0700 (PDT) Received: by nimloth.alvh.no-ip.org (Postfix, from userid 1000) id 214CD300805; Fri, 13 Mar 2020 13:25:20 -0300 (-03) Date: Fri, 13 Mar 2020 13:25:20 -0300 From: Alvaro Herrera To: Ashutosh Bapat Cc: Pg Hackers , ashutosh.bapat@2ndquadrant.com Subject: Re: BEFORE ROW triggers for partitioned tables Message-ID: <20200313162520.GA22557@alvherre.pgsql> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="9amGYk9869ThD9tj" Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: User-Agent: Mutt/1.10.1 (2018-07-13) List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Precedence: bulk --9amGYk9869ThD9tj Content-Type: text/plain; charset=iso-8859-1 Content-Disposition: inline Content-Transfer-Encoding: 8bit On 2020-Mar-11, Ashutosh Bapat wrote: > On Thu, Feb 27, 2020 at 10:22 PM Alvaro Herrera > wrote: > > * The new function I added, ReportTriggerPartkeyChange(), contains one > > serious bug (namely: it doesn't map attribute numbers properly if > > partitions are differently defined). > > IIUC the code in your patch, it seems you are just looking at > partnatts. But partition key can contain expressions also which are > stored in partexprs. So, I think the code won't catch change in the > partition key values when it contains expression. Using > RelationGetPartitionQual() will fix this problem and also problem of > attribute remapping across the partition hierarchy. Oh, of course. In fact, I don't need to deal with PartitionQual directly; I can just use ExecPartitionCheck(), since in ExecBRInsertTriggers et al we already have all we need. v2 attached. Here's some example output. With my previous patch, this was the error we reported: insert into parted values (1, 1, 'uno uno v2'); -- fail ERROR: changing partitioning columns in a before trigger is not supported DETAIL: Column "a" was changed by trigger "t". Now, passing emitError=true to ExecPartitionCheck, I get this: insert into parted values (1, 1, 'uno uno v2'); -- fail ERROR: new row for relation "parted_1_1" violates partition constraint DETAIL: Failing row contains (2, 1, uno uno v2). Note the discrepancy in the table named in the INSERT vs. the one in the error message. This is a low-quality error IMO. So I'd instead pass emitError=false, and produce my own error. It's useful to report the trigger name and original partition name: insert into parted values (1, 1, 'uno uno v2'); -- fail ERROR: moving row to another partition during a BEFORE trigger is not supported DETAIL: Before trigger "t", row was to be in partition "public.parted_1_1" Note that in this implementation I no longer know which column is the problematic one, but I suppose users have clue enough. Wording suggestions welcome. -- Álvaro Herrera https://www.2ndQuadrant.com/ PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services --9amGYk9869ThD9tj Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="v2-0001-Enable-BEFORE-row-level-triggers-for-partitioned-.patch" From f13d7ad4c081ede118b0f6c262ad92a49ea9f64c Mon Sep 17 00:00:00 2001 From: Alvaro Herrera Date: Wed, 26 Feb 2020 17:34:54 -0300 Subject: [PATCH v2] Enable BEFORE row-level triggers for partitioned tables ... with the limitation that if partitioning columns are changed, the operation is aborted. (The reason for this limitation is that it might require re-routing the tuple, which doesn't work.) --- src/backend/commands/tablecmds.c | 7 ---- src/backend/commands/trigger.c | 40 +++++++++++++------ src/backend/partitioning/partdesc.c | 2 +- src/include/utils/reltrigger.h | 1 + src/test/regress/expected/triggers.out | 53 ++++++++++++++++++++++++-- src/test/regress/sql/triggers.sql | 39 ++++++++++++++++++- 6 files changed, 116 insertions(+), 26 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 3eb861bfbf..08ad595596 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -16501,13 +16501,6 @@ CloneRowTriggersToPartition(Relation parent, Relation partition) !OidIsValid(trigForm->tgparentid))) continue; - /* - * Complain if we find an unexpected trigger type. - */ - if (!TRIGGER_FOR_AFTER(trigForm->tgtype)) - elog(ERROR, "unexpected trigger \"%s\" found", - NameStr(trigForm->tgname)); - /* Use short-lived context for CREATE TRIGGER */ oldcxt = MemoryContextSwitchTo(perTupCxt); diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 513427edf1..98911facad 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -221,18 +221,6 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString, */ if (stmt->row) { - /* - * BEFORE triggers FOR EACH ROW are forbidden, because they would - * allow the user to direct the row to another partition, which - * isn't implemented in the executor. - */ - if (stmt->timing != TRIGGER_TYPE_AFTER) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is a partitioned table", - RelationGetRelationName(rel)), - errdetail("Partitioned tables cannot have BEFORE / FOR EACH ROW triggers."))); - /* * Disallow use of transition tables. * @@ -1658,6 +1646,7 @@ RelationBuildTriggers(Relation relation) build->tgtype = pg_trigger->tgtype; build->tgenabled = pg_trigger->tgenabled; build->tgisinternal = pg_trigger->tgisinternal; + build->tgisclone = OidIsValid(pg_trigger->tgparentid); build->tgconstrrelid = pg_trigger->tgconstrrelid; build->tgconstrindid = pg_trigger->tgconstrindid; build->tgconstraint = pg_trigger->tgconstraint; @@ -1961,6 +1950,8 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2) return false; if (trig1->tgisinternal != trig2->tgisinternal) return false; + if (trig1->tgisclone != trig2->tgisclone) + return false; if (trig1->tgconstrrelid != trig2->tgconstrrelid) return false; if (trig1->tgconstrindid != trig2->tgconstrindid) @@ -2247,6 +2238,21 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo, { ExecForceStoreHeapTuple(newtuple, slot, false); + /* + * After a tuple in a partition goes through a trigger, the user + * could have changed the partition key enough that the tuple + * no longer fits the partition. Verify that. + */ + if (trigger->tgisclone && + !ExecPartitionCheck(relinfo, slot, estate, false)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("moving row to another partition during a BEFORE trigger is not supported"), + errdetail("Before trigger \"%s\", row was to be in partition \"%s.%s\"", + trigger->tgname, + get_namespace_name(RelationGetNamespace(relinfo->ri_RelationDesc)), + RelationGetRelationName(relinfo->ri_RelationDesc)))); + if (should_free) heap_freetuple(oldtuple); @@ -2741,6 +2747,16 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, { ExecForceStoreHeapTuple(newtuple, newslot, false); + if (trigger->tgisclone && + !ExecPartitionCheck(relinfo, newslot, estate, false)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("moving row to another partition during a BEFORE trigger is not supported"), + errdetail("Before trigger \"%s\", row was to be in partition \"%s.%s\"", + trigger->tgname, + get_namespace_name(RelationGetNamespace(relinfo->ri_RelationDesc)), + RelationGetRelationName(relinfo->ri_RelationDesc)))); + /* * If the tuple returned by the trigger / being stored, is the old * row version, and the heap tuple passed to the trigger was diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c index bdda42e40f..e7f23542e8 100644 --- a/src/backend/partitioning/partdesc.c +++ b/src/backend/partitioning/partdesc.c @@ -307,7 +307,7 @@ CreatePartitionDirectory(MemoryContext mcxt) * * The purpose of this function is to ensure that we get the same * PartitionDesc for each relation every time we look it up. In the - * face of current DDL, different PartitionDescs may be constructed with + * face of concurrent DDL, different PartitionDescs may be constructed with * different views of the catalog state, but any single particular OID * will always get the same PartitionDesc for as long as the same * PartitionDirectory is used. diff --git a/src/include/utils/reltrigger.h b/src/include/utils/reltrigger.h index 28df43d833..b22acb034e 100644 --- a/src/include/utils/reltrigger.h +++ b/src/include/utils/reltrigger.h @@ -29,6 +29,7 @@ typedef struct Trigger int16 tgtype; char tgenabled; bool tgisinternal; + bool tgisclone; Oid tgconstrrelid; Oid tgconstrindid; Oid tgconstraint; diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out index 22e65cc1ec..c51e3acb14 100644 --- a/src/test/regress/expected/triggers.out +++ b/src/test/regress/expected/triggers.out @@ -1958,10 +1958,6 @@ drop table my_table; create table parted_trig (a int) partition by list (a); create function trigger_nothing() returns trigger language plpgsql as $$ begin end; $$; -create trigger failed before insert or update or delete on parted_trig - for each row execute procedure trigger_nothing(); -ERROR: "parted_trig" is a partitioned table -DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers. create trigger failed instead of update on parted_trig for each row execute procedure trigger_nothing(); ERROR: "parted_trig" is a table @@ -2246,6 +2242,55 @@ NOTICE: aasvogel <- woof! NOTICE: trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel) NOTICE: trigger parted_trig_odd on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel) drop table parted_irreg_ancestor; +-- Before triggers and partitions +create table parted (a int, b int, c text) partition by list (a); +create table parted_1 partition of parted for values in (1) + partition by list (b); +create table parted_1_1 partition of parted_1 for values in (1); +create function parted_trigfunc() returns trigger language plpgsql as $$ +begin + new.a = new.a + 1; + return new; +end; +$$; +insert into parted values (1, 1, 'uno uno'); -- works +create trigger t before insert or update or delete on parted + for each row execute procedure parted_trigfunc(); +insert into parted values (1, 1, 'uno uno v2'); -- fail +ERROR: moving row to another partition during a BEFORE trigger is not supported +DETAIL: Before trigger "t", row was to be in partition "public.parted_1_1" +update parted set c = c || 'v3'; -- fail +ERROR: moving row to another partition during a BEFORE trigger is not supported +DETAIL: Before trigger "t", row was to be in partition "public.parted_1_1" +create or replace function parted_trigfunc() returns trigger language plpgsql as $$ +begin + new.b = new.b + 1; + return new; +end; +$$; +insert into parted values (1, 1, 'uno uno v4'); -- fail +ERROR: moving row to another partition during a BEFORE trigger is not supported +DETAIL: Before trigger "t", row was to be in partition "public.parted_1_1" +update parted set c = c || 'v5'; -- fail +ERROR: moving row to another partition during a BEFORE trigger is not supported +DETAIL: Before trigger "t", row was to be in partition "public.parted_1_1" +create or replace function parted_trigfunc() returns trigger language plpgsql as $$ +begin + new.c = new.c || ' and so'; + return new; +end; +$$; +insert into parted values (1, 1, 'uno uno'); -- works +update parted set c = c || ' v6'; -- works +select tableoid::regclass, * from parted; + tableoid | a | b | c +------------+---+---+-------------------------- + parted_1_1 | 1 | 1 | uno uno v6 and so + parted_1_1 | 1 | 1 | uno uno and so v6 and so +(2 rows) + +drop table parted; +drop function parted_trigfunc(); -- -- Constraint triggers and partitioned tables create table parted_constr_ancestor (a int, b text) diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql index 0f61fdf0ea..3d9f14618c 100644 --- a/src/test/regress/sql/triggers.sql +++ b/src/test/regress/sql/triggers.sql @@ -1348,8 +1348,6 @@ drop table my_table; create table parted_trig (a int) partition by list (a); create function trigger_nothing() returns trigger language plpgsql as $$ begin end; $$; -create trigger failed before insert or update or delete on parted_trig - for each row execute procedure trigger_nothing(); create trigger failed instead of update on parted_trig for each row execute procedure trigger_nothing(); create trigger failed after update on parted_trig @@ -1561,6 +1559,43 @@ insert into parted1_irreg values ('aardwolf', 2); insert into parted_irreg_ancestor values ('aasvogel', 3); drop table parted_irreg_ancestor; +-- Before triggers and partitions +create table parted (a int, b int, c text) partition by list (a); +create table parted_1 partition of parted for values in (1) + partition by list (b); +create table parted_1_1 partition of parted_1 for values in (1); +create function parted_trigfunc() returns trigger language plpgsql as $$ +begin + new.a = new.a + 1; + return new; +end; +$$; +insert into parted values (1, 1, 'uno uno'); -- works +create trigger t before insert or update or delete on parted + for each row execute procedure parted_trigfunc(); +insert into parted values (1, 1, 'uno uno v2'); -- fail +update parted set c = c || 'v3'; -- fail +create or replace function parted_trigfunc() returns trigger language plpgsql as $$ +begin + new.b = new.b + 1; + return new; +end; +$$; +insert into parted values (1, 1, 'uno uno v4'); -- fail +update parted set c = c || 'v5'; -- fail +create or replace function parted_trigfunc() returns trigger language plpgsql as $$ +begin + new.c = new.c || ' and so'; + return new; +end; +$$; +insert into parted values (1, 1, 'uno uno'); -- works +update parted set c = c || ' v6'; -- works +select tableoid::regclass, * from parted; + +drop table parted; +drop function parted_trigfunc(); + -- -- Constraint triggers and partitioned tables create table parted_constr_ancestor (a int, b text) -- 2.20.1 --9amGYk9869ThD9tj--