a misbehavior of partition row movement (?)
Hi,
Robert forwarded me a pgsql-general thread [1]/messages/by-id/CAL54xNZsLwEM1XCk5yW9EqaRzsZYHuWsHQkA2L5MOSKXAwviCQ@mail.gmail.com where a ON DELETE
CASCADE specified on a foreign key pointing to a partitioned table is
shown to cause a possibly surprising end result during an update of
the partitioned table. Example from that thread:
create table parent ( id serial, constraint parent_pkey primary key
(id)) partition by range (id);
create table parent_10 partition of parent for values from (0) to (10);
create table parent_20 partition of parent for values from (11) to (20);
create table child (id serial, parent_id int constraint parent_id_fk
references parent(id) on update cascade on delete cascade);
insert into parent values(0);
insert into child values(1,0);
update parent set id = 5; -- no row movement, so normal update
table parent;
id
----
5
(1 row)
table child;
id | parent_id
----+-----------
1 | 5
(1 row)
update parent set id = 15; -- row movement, so delete+insert
table parent;
id
----
15
(1 row)
table child; -- ON DELETE CASCADE having done its job
id | parent_id
----+-----------
(0 rows)
Reporter on that thread says that the last update should have failed
and I don't quite see a workable alternative to that. What we could
do is check before calling ExecDelete() that will perform the DELETE
part of the row movement if the foreign key action trigger that
implements the ON DELETE CASCADE action (an internal trigger) is among
the AR delete triggers that will run as part of that DELETE. If yes,
abort the operation. See attached a patch for that. I'm not terribly
happy with the error and details messages though:
update parent set id = 15;
ERROR: cannot move row being updated to another partition
DETAIL: Moving the row may cause a foreign key involving the source
partition to be violated.
Thoughts?
--
Amit Langote
EDB: http://www.enterprisedb.com
[1]: /messages/by-id/CAL54xNZsLwEM1XCk5yW9EqaRzsZYHuWsHQkA2L5MOSKXAwviCQ@mail.gmail.com
Attachments:
prevent-row-movement-on-delete-cascade.patchapplication/octet-stream; name=prevent-row-movement-on-delete-cascade.patchDownload
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 8fa43ab..68e0924 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -54,6 +54,7 @@
#include "storage/lmgr.h"
#include "utils/builtins.h"
#include "utils/datum.h"
+#include "utils/fmgroids.h"
#include "utils/memutils.h"
#include "utils/rel.h"
@@ -1283,7 +1284,42 @@ lreplace:;
/*
* Row movement, part 1. Delete the tuple, but skip RETURNING
* processing. We want to return rows from INSERT.
+ *
+ * However, deleting the tuple might inadvertently delete a tuple
+ * in a referencing relation due to the relevant foreign key's ON
+ * DELETE CASCADE action. That should not be allowed to happen,
+ * because the row being moved is not actually being "deleted" as
+ * such from this relation. We can check if that may happen by
+ * looking for a constraint trigger in this relation that
+ * implements the cascaded delete.
*/
+ if (resultRelInfo->ri_TrigDesc != NULL &&
+ resultRelInfo->ri_TrigDesc->trig_delete_after_row)
+ {
+ TriggerDesc *trigdesc = resultRelInfo->ri_TrigDesc;
+ int i;
+ bool found = false;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[i];
+
+ if (trigger->tgisinternal &&
+ OidIsValid(trigger->tgconstrrelid) &&
+ trigger->tgfoid == F_RI_FKEY_CASCADE_DEL)
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (found)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move row being updated to another partition"),
+ errdetail("Moving the row may cause a foreign key involving the source partition to be violated.")));
+ }
+
ExecDelete(mtstate, tupleid, oldtuple, planSlot, epqstate,
estate, false, false /* canSetTag */ ,
true /* changingPart */ , &tuple_deleted, &epqslot);
diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out
index dc34ac6..28c0f30 100644
--- a/src/test/regress/expected/update.out
+++ b/src/test/regress/expected/update.out
@@ -942,3 +942,21 @@ update hash_parted set b = b + 8 where b = 1;
drop table hash_parted;
drop operator class custom_opclass using hash;
drop function dummy_hashint4(a int4, seed int8);
+-- Disallow moving a row that is referenced in a foreign key relationship
+create table fk_parted_parent (id int primary key) partition by list (id);
+create table fk_parted_parent_1 partition of fk_parted_parent for values in (1, 2);
+create table fk_parted_paernt_2 partition of fk_parted_parent for values in (3);
+create table fk_child (a int, id int references fk_parted_parent on delete cascade on update cascade);
+insert into fk_parted_parent values (1);
+insert into fk_child values (1, 1);
+update fk_parted_parent set id = 2; -- ok
+table fk_child;
+ a | id
+---+----
+ 1 | 2
+(1 row)
+
+update fk_parted_parent set id = 3; -- error
+ERROR: cannot move row being updated to another partition
+DETAIL: Moving the row may cause a foreign key involving the source partition to be violated.
+drop table fk_child, fk_parted_parent;
diff --git a/src/test/regress/sql/update.sql b/src/test/regress/sql/update.sql
index 8c558a7..f353e3b 100644
--- a/src/test/regress/sql/update.sql
+++ b/src/test/regress/sql/update.sql
@@ -612,3 +612,15 @@ update hash_parted set b = b + 8 where b = 1;
drop table hash_parted;
drop operator class custom_opclass using hash;
drop function dummy_hashint4(a int4, seed int8);
+
+-- Disallow moving a row that is referenced in a foreign key relationship
+create table fk_parted_parent (id int primary key) partition by list (id);
+create table fk_parted_parent_1 partition of fk_parted_parent for values in (1, 2);
+create table fk_parted_paernt_2 partition of fk_parted_parent for values in (3);
+create table fk_child (a int, id int references fk_parted_parent on delete cascade on update cascade);
+insert into fk_parted_parent values (1);
+insert into fk_child values (1, 1);
+update fk_parted_parent set id = 2; -- ok
+table fk_child;
+update fk_parted_parent set id = 3; -- error
+drop table fk_child, fk_parted_parent;
On Friday, October 2, 2020, Amit Langote <amitlangote09@gmail.com> wrote:
Reporter on that thread says that the last update should have failed
and I don't quite see a workable alternative to that.
To be clear the OP would rather have it just work, the same as the
non-row-movement version. Maybe insert the new row first, execute the on
update trigger chained from the old row, then delete the old row?
David J.
On Fri, Oct 2, 2020 at 11:32 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
On Friday, October 2, 2020, Amit Langote <amitlangote09@gmail.com> wrote:
Reporter on that thread says that the last update should have failed
and I don't quite see a workable alternative to that.To be clear the OP would rather have it just work, the same as the non-row-movement version. Maybe insert the new row first, execute the on update trigger chained from the old row, then delete the old row?
I was thinking yesterday about making it just work, but considering
the changes that would need to be made to how the underlying triggers
fire, it does not seem we would be able to back-port the solution.
--
Amit Langote
EDB: http://www.enterprisedb.com
On Sat, Oct 03, 2020 at 11:42:21AM +0900, Amit Langote wrote:
On Fri, Oct 2, 2020 at 11:32 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:On Friday, October 2, 2020, Amit Langote <amitlangote09@gmail.com>
wrote:Reporter on that thread says that the last update should have failed
and I don't quite see a workable alternative to that.To be clear the OP would rather have it just work, the same as the
non-row-movement version. Maybe insert the new row first, execute
the on update trigger chained from the old row, then delete the old
row?I was thinking yesterday about making it just work, but considering the
changes that would need to be made to how the underlying triggers fire,
it does not seem we would be able to back-port the solution.
I think we need to differentiate between master and backbranches. IMO we
should try to make it "just work" in master, and the amount of code
should not be an issue there I think (no opinion on whether insert and
update trigger is the way to go). For backbranches we may need to do
something less intrusive, of course.
regards
--
Tomas Vondra http://www.2ndQuadrant.com
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Sat, Oct 3, 2020 at 8:15 PM Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote
On Sat, Oct 03, 2020 at 11:42:21AM +0900, Amit Langote wrote:
On Fri, Oct 2, 2020 at 11:32 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:On Friday, October 2, 2020, Amit Langote <amitlangote09@gmail.com>
wrote:Reporter on that thread says that the last update should have failed
and I don't quite see a workable alternative to that.To be clear the OP would rather have it just work, the same as the
non-row-movement version. Maybe insert the new row first, execute
the on update trigger chained from the old row, then delete the old
row?I was thinking yesterday about making it just work, but considering the
changes that would need to be made to how the underlying triggers fire,
it does not seem we would be able to back-port the solution.I think we need to differentiate between master and backbranches. IMO we
should try to make it "just work" in master, and the amount of code
should not be an issue there I think (no opinion on whether insert and
update trigger is the way to go). For backbranches we may need to do
something less intrusive, of course.
Sure, that makes sense. I will try making a patch for HEAD to make it
just work unless someone beats me to it.
--
Amit Langote
EDB: http://www.enterprisedb.com
On Sat, Oct 3, 2020 at 8:26 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Sat, Oct 3, 2020 at 8:15 PM Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote
On Sat, Oct 03, 2020 at 11:42:21AM +0900, Amit Langote wrote:
On Fri, Oct 2, 2020 at 11:32 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:On Friday, October 2, 2020, Amit Langote <amitlangote09@gmail.com>
wrote:Reporter on that thread says that the last update should have failed
and I don't quite see a workable alternative to that.To be clear the OP would rather have it just work, the same as the
non-row-movement version. Maybe insert the new row first, execute
the on update trigger chained from the old row, then delete the old
row?I was thinking yesterday about making it just work, but considering the
changes that would need to be made to how the underlying triggers fire,
it does not seem we would be able to back-port the solution.I think we need to differentiate between master and backbranches. IMO we
should try to make it "just work" in master, and the amount of code
should not be an issue there I think (no opinion on whether insert and
update trigger is the way to go). For backbranches we may need to do
something less intrusive, of course.Sure, that makes sense. I will try making a patch for HEAD to make it
just work unless someone beats me to it.
After working on this for a while, here is my proposal for HEAD.
To reiterate, the problem occurs when an UPDATE of a partitioned PK
table is turned into a DELETE + INSERT. In that case, the UPDATE RI
triggers are not fired at all, but DELETE ones are, so the foreign key
may fail to get enforced correctly. For example, even though the new
row after the update is still logically present in the PK table, it
would wrongly get deleted because of the DELETE RI trigger firing if
there's a ON DELETE CASCADE clause on the foreign key.
To fix that, I propose that we teach trigger.c to skip queuing the
events that would be dangerous to fire, such as that for the DELETE on
the source leaf partition mentioned above. Instead, queue an UPDATE
event on the root target table, matching the actual operation being
performed. Note though that this new arrangement doesn't affect the
firing of any other triggers except those that are relevant to the
reported problem, viz. the PK-side RI triggers. All other triggers
fire exactly as they did before.
To make that happen, I had to:
1. Make RI triggers available on partitioned tables at all, which they
are not today. Also, mark the RI triggers in partitions correctly as
*clones* of the RI triggers in their respective parents.
2. Make it possible to allow AfterTriggerSaveEvent() to access the
query's actual target relation, that is, in addition to the target
relation on which an event was fired. Also, added a bunch of code to
AFTER TRIGGER infrastructure to handle events fired on partitioned
tables. Because those events cannot contain physical references to
affected tuples, I generalized the code currently used to handle after
triggers on foreign tables by storing the tuples in and retrieving
them from a tuple store. I read a bunch of caveats of that
implementation (such as its uselessness for deferred triggers), but
for the limited cases for which it will be used for partitioned
tables, it seems safe, because it won't be used for deferred triggers
on partitioned tables.
Attached patches 0001 and 0002 implement 1 and 2, respectively.
Later, I will post an updated version of the patch for the
back-branches, which, as mentioned upthread, is to prevent the
cross-partition updates on foreign key PK tables.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v1-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v1-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From 5b5c735589d46aee1ee1769b7663f4b041c44496 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v1 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 354 +++++++++++++++++++++++++++------
src/backend/commands/trigger.c | 101 +++++++++-
src/bin/pg_dump/pg_dump.c | 20 +-
src/bin/psql/describe.c | 6 +-
src/include/commands/trigger.h | 5 +-
src/test/regress/expected/triggers.out | 4 +-
6 files changed, 407 insertions(+), 83 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 46f1637..5c1848c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -450,12 +450,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -463,15 +465,34 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
+static void AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -8762,7 +8783,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8775,7 +8797,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -8815,7 +8838,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -8823,6 +8847,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -8922,12 +8948,11 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* If the referenced table is a plain relation, create the action triggers
* that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -8971,7 +8996,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9020,8 +9046,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9035,14 +9065,16 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9071,6 +9103,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9112,7 +9153,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9191,10 +9235,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9250,6 +9298,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9279,6 +9328,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9296,6 +9353,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9362,6 +9421,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ GetForeignKeyActionTriggers(constrForm, trigrel,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9374,11 +9437,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9401,6 +9468,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9423,6 +9491,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
errmsg("foreign key constraints are not supported on foreign tables")));
/*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
*/
@@ -9451,6 +9527,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9479,6 +9557,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9497,7 +9579,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9596,9 +9681,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9619,13 +9708,15 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
@@ -9689,12 +9780,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -9727,13 +9816,135 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
+
+ AttachForeignKeyCheckTriggers(fk, partRelid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
CommandCounterIncrement();
return true;
}
+static void
+GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(constrForm->oid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != constrForm->conrelid)
+ continue;
+ if (trgform->tgrelid != constrForm->confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key constraint %u",
+ constrForm->oid);
+
+ systable_endscan(scan);
+}
+
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+static void
+AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+}
+
+static void
+DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid)
+{
+ Relation trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, partRelid);
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, partRelid);
+
+ table_close(trigrel, RowExclusiveLock);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10534,10 +10745,12 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10577,11 +10790,14 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
@@ -10591,9 +10807,12 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10645,9 +10864,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10702,9 +10924,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
@@ -10715,12 +10940,16 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -16819,20 +17048,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
if (!TRIGGER_FOR_ROW(trigForm->tgtype))
continue;
- /*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
- */
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ /* Don't clone internal triggers. They are dealt with separately. */
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17081,7 +17298,11 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
+
+ DetachForeignKeyCheckTriggers(fk, RelationGetRelid(partRel));
ReleaseSysCache(contup);
}
@@ -17158,6 +17379,13 @@ DropClonedTriggersFromPartition(Oid partitionId)
continue;
/*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys. They will be dealt with separately.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
+ /*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
*/
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c336b23..7b2d0de 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -136,8 +136,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -188,6 +190,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -726,6 +729,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -752,17 +756,16 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -859,7 +862,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1228,6 +1231,82 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ Assert(trigForm->tgparentid == 0);
+ if (trigForm->tgparentid != InvalidOid)
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index dc1d41d..291405c 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7905,7 +7905,25 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 90000)
+ if (fout->remoteVersion >= 130000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ */
+ appendPQExpBuffer(query,
+ "SELECT tgname, "
+ "tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(oid, false) AS tgdef, "
+ "tgenabled, tableoid, oid "
+ "FROM pg_catalog.pg_trigger t "
+ "WHERE tgrelid = '%u'::pg_catalog.oid "
+ "AND NOT tgisinternal "
+ "AND tgparentid = 0",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 90000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 07d6400..94d1afe 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2969,11 +2969,7 @@ describeOneTableDetails(const char *schemaname,
" 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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
- else if (pset.sversion >= 90000)
+ if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
else if (pset.sversion >= 80300)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index e38d732..67cdb2d 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -154,7 +154,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
-
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 213dff6..decb140 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3216,7 +3216,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
1.8.3.1
v1-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v1-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From 6fdaf679c3bc72f4b680d539f07790f3fa3d2129 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v1 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned primary key table referenced in a
foreign key constraint causes a row to move from one partition to
another, instead of firing an UPDATE after-trigger event, DELETE and
INSERT events are fired on the source and the destination leaf
partition, respectively, which can result in pretty surprising
outcomes. To be fair, it would be wrong to fire the UPDATE event
on the source leaf partition itself, because the new row is not
inserted into it.
This commit teaches trigger.c to skip queuing the aforementioned
DELETE and INSERT events on the leaf partitions in favor of an
UPDATE event fired on the "root" target relation, which makes sense
because both the old and new tuple "logically" belong to it. To
make that possible, this adjusts AFTER trigger data strucutures to
allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. The use case of distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root partitioned table is perhaps of minor importance.
---
src/backend/commands/trigger.c | 162 +++++++++++++++--------
src/backend/executor/execMain.c | 9 ++
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 212 ++++++++++++++++++++++++++++--
src/backend/utils/adt/ri_triggers.c | 17 ++-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 4 +
src/test/regress/expected/foreign_key.out | 146 +++++++++++++++++++-
src/test/regress/sql/foreign_key.sql | 84 ++++++++++++
9 files changed, 569 insertions(+), 71 deletions(-)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7b2d0de..58277be 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -98,7 +98,9 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2317,7 +2319,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2406,7 +2408,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2531,7 +2533,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2628,7 +2630,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2651,7 +2654,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2766,7 +2769,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
GetAllUpdatedColumns(relinfo, estate),
transition_capture);
@@ -2913,7 +2916,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2947,7 +2951,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else if (fdw_trigtuple != NULL)
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
GetAllUpdatedColumns(relinfo, estate),
transition_capture);
@@ -3073,7 +3077,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3361,19 +3365,21 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. For partitioned tables, we never
+ * queue any events for its deferred triggers. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3394,8 +3400,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3589,7 +3595,8 @@ typedef struct AfterTriggersData
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3638,15 +3645,15 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
+ * Get the tuplestore for the current trigger query level, creating it
* if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetCurrentAfterTriggerTuplestore(void)
{
Tuplestorestate *ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
+ ret = afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
if (ret == NULL)
{
MemoryContext oldcxt;
@@ -3665,7 +3672,7 @@ GetCurrentFDWTuplestore(void)
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
+ afterTriggers.query_stack[afterTriggers.query_depth].tuplestore = ret;
}
return ret;
@@ -3999,22 +4006,22 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore = GetCurrentAfterTriggerTuplestore();
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4315,7 +4322,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4699,8 +4707,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5032,7 +5040,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5503,7 +5511,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5517,7 +5526,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5715,7 +5724,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5735,16 +5746,62 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ /*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ mtstate->rootResultRelInfo->ri_RelationDesc->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)
+ {
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ continue;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+ break;
+ }
+ }
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ if (tuplestore == NULL)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ tuplestore = GetCurrentAfterTriggerTuplestore();
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5818,16 +5875,17 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(tuplestore, newslot);
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 7179f58..14f7680 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1438,8 +1438,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 01d2688..3f53754 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 29e07b7..eb695d7 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -42,6 +42,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -380,7 +381,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -660,7 +663,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -698,6 +701,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -992,7 +1000,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1007,7 +1015,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1079,7 +1087,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
@@ -1168,8 +1178,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1182,6 +1193,96 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1335,9 +1436,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1349,14 +1453,97 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * Enforce foreign key actions using the root relation's triggers.
+ * NULL insert_destrel means that the move failed to occur or that
+ * the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ {
+ ListCell *lc;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(resultRelInfo,
+ mtstate);
+
+ /*
+ * There better not be any foreign keys that point into some
+ * non-root ancestor but not root (tgisclone is false), because
+ * we can't enforce them (for now) like we can those on the
+ * root parent.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted tuple ("new" tuple) into the root table's
+ * slot, possibly converting it.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(resultRelInfo, mtstate);
+ if (inserted_tuple != slot && map)
+ slot = execute_attr_map_slot(map->attrMap, inserted_tuple,
+ rootslot);
+ else
+ slot = ExecCopySlot(rootslot, inserted_tuple);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(resultRelationDesc,
+ tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ slot, NIL, NULL);
+ }
+
+ return returning_slot;
}
/*
@@ -1522,7 +1709,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2127,7 +2315,7 @@ ExecModifyTable(PlanState *pstate)
{
case CMD_INSERT:
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 7e2b2e3..e2f9eb9 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1259,11 +1259,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 67cdb2d..43bd043 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -205,6 +205,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -224,6 +225,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 6c0a7d6..0922a3e 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -497,9 +497,13 @@ typedef struct ResultRelInfo
* transition tuple capture or update partition row movement is active.
*/
TupleConversionMap *ri_ChildToRootMap;
+ bool ri_ChildToRootMapValid;
/* for use by copy.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 07bd5b6..e2c2c3d 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2407,7 +2407,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2470,3 +2470,147 @@ DROP SCHEMA fkpart9 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart9.pk
drop cascades to table fkpart9.fk
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart10.pk12 | 2
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 2
+ fkpart10.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 2
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 2
+ fkpart10.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+ tableoid | a
+---------------+---
+ fkpart10.pk11 | 1
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 1
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 4
+ fkpart10.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart10.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart10 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart10.pk
+drop cascades to table fkpart10.fk_parted
+drop cascades to table fkpart10.fk_another
+drop cascades to function fkpart10.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index c5c9011..5902b78 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1738,3 +1738,87 @@ DELETE FROM fkpart9.pk WHERE a=35;
SELECT * FROM fkpart9.pk;
SELECT * FROM fkpart9.fk;
DROP SCHEMA fkpart9 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+DROP TABLE fkpart10.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart10 CASCADE;
--
1.8.3.1
On Fri, Nov 20, 2020 at 8:55 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Sat, Oct 3, 2020 at 8:26 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Sat, Oct 3, 2020 at 8:15 PM Tomas Vondra <tomas.vondra@2ndquadrant.com> wrote
I think we need to differentiate between master and backbranches. IMO we
should try to make it "just work" in master, and the amount of code
should not be an issue there I think (no opinion on whether insert and
update trigger is the way to go). For backbranches we may need to do
something less intrusive, of course.Sure, that makes sense. I will try making a patch for HEAD to make it
just work unless someone beats me to it.After working on this for a while, here is my proposal for HEAD.
To reiterate, the problem occurs when an UPDATE of a partitioned PK
table is turned into a DELETE + INSERT. In that case, the UPDATE RI
triggers are not fired at all, but DELETE ones are, so the foreign key
may fail to get enforced correctly. For example, even though the new
row after the update is still logically present in the PK table, it
would wrongly get deleted because of the DELETE RI trigger firing if
there's a ON DELETE CASCADE clause on the foreign key.To fix that, I propose that we teach trigger.c to skip queuing the
events that would be dangerous to fire, such as that for the DELETE on
the source leaf partition mentioned above. Instead, queue an UPDATE
event on the root target table, matching the actual operation being
performed. Note though that this new arrangement doesn't affect the
firing of any other triggers except those that are relevant to the
reported problem, viz. the PK-side RI triggers. All other triggers
fire exactly as they did before.To make that happen, I had to:
1. Make RI triggers available on partitioned tables at all, which they
are not today. Also, mark the RI triggers in partitions correctly as
*clones* of the RI triggers in their respective parents.2. Make it possible to allow AfterTriggerSaveEvent() to access the
query's actual target relation, that is, in addition to the target
relation on which an event was fired. Also, added a bunch of code to
AFTER TRIGGER infrastructure to handle events fired on partitioned
tables. Because those events cannot contain physical references to
affected tuples, I generalized the code currently used to handle after
triggers on foreign tables by storing the tuples in and retrieving
them from a tuple store. I read a bunch of caveats of that
implementation (such as its uselessness for deferred triggers), but
for the limited cases for which it will be used for partitioned
tables, it seems safe, because it won't be used for deferred triggers
on partitioned tables.Attached patches 0001 and 0002 implement 1 and 2, respectively.
Later, I will post an updated version of the patch for the
back-branches, which, as mentioned upthread, is to prevent the
cross-partition updates on foreign key PK tables.
I have created a CF entry for this:
https://commitfest.postgresql.org/31/2877/
--
Amit Langote
EDB: http://www.enterprisedb.com
Hi,
thanks for looking into this. I haven't yet looked at your patch in detail, yet I think we should address the general issue here. To me this doesn't seem to be a RI-trigger issue, but a more general issue like I mentioned in the pg-bugs thread /messages/by-id/b1bfc99296e34725900bcd9689be8420@index.de
While I like the idea of making fks work, I'd prefer a solution that fires the appropriate row trigger for partitioned tables for non RI-cases as well.
Regards
Arne
Hi,
On Tue, Dec 15, 2020 at 12:01 AM Arne Roland <A.Roland@index.de> wrote:
thanks for looking into this. I haven't yet looked at your patch in detail, yet I think we should address the general issue here. To me this doesn't seem to be a RI-trigger issue, but a more general issue like I mentioned in the pg-bugs thread /messages/by-id/b1bfc99296e34725900bcd9689be8420@index.de
Hmm, maybe you're reading too much into the implementation details of
the fix, because the patch does fix the issue that you mention in the
linked report:
Quoting your original example:
drop table a, b;
create table a (id serial, primary key (id)) partition by range (id);
create table b (id serial, primary key (id)) partition by range (id);
alter table b add constraint a_fk foreign key (id) references a (id)
on delete cascade;
create table a1 partition of a for values from (1) to (2);
create table a2 partition of a for values from (2) to (3);
create table b1 partition of b for values from (1) to (2);
create table b2 partition of b for values from (2) to (3);
insert into a (id) values (1);
insert into b (id) values (1);
-- correctly errors out instead of silently performing the ON DELETE CASCADE
update a set id=2;
ERROR: update or delete on table "a" violates foreign key constraint
"a_fk" on table "b"
DETAIL: Key (id)=(1) is still referenced from table "b".
select * from b;
id
----
1
(1 row)
Changing the example to specify ON UPDATE CASCADE:
drop table a, b;
create table a (id serial, primary key (id)) partition by range (id);
create table b (id serial, primary key (id)) partition by range (id);
alter table b add constraint a_fk foreign key (id) references a (id)
on delete cascade;
create table a1 partition of a for values from (1) to (2);
create table a2 partition of a for values from (2) to (3);
create table b1 partition of b for values from (1) to (2);
create table b2 partition of b for values from (2) to (3);
insert into a (id) values (1);
insert into b (id) values (1);
-- correctly applies ON UPDATE CASCADE action
update a set id=2;
UPDATE 1
select * from b;
id
----
2
(1 row)
What am I missing about what you think is the more general problem to be solved?
While I like the idea of making fks work, I'd prefer a solution that fires the appropriate row trigger for partitioned tables for non RI-cases as well.
Maybe we could do that, but I don't know of a known issue where the
root cause is our firing of a row trigger on the leaf partition
instead of on the root partitioned table.
--
Amit Langote
EDB: http://www.enterprisedb.com
On Tue, Dec 15, 2020 at 12:43 PM Amit Langote <amitlangote09@gmail.com> wrote:
Quoting your original example:
drop table a, b;
create table a (id serial, primary key (id)) partition by range (id);
create table b (id serial, primary key (id)) partition by range (id);
alter table b add constraint a_fk foreign key (id) references a (id)
on delete cascade;
create table a1 partition of a for values from (1) to (2);
create table a2 partition of a for values from (2) to (3);
create table b1 partition of b for values from (1) to (2);
create table b2 partition of b for values from (2) to (3);
insert into a (id) values (1);
insert into b (id) values (1);-- correctly errors out instead of silently performing the ON DELETE CASCADE
update a set id=2;
ERROR: update or delete on table "a" violates foreign key constraint
"a_fk" on table "b"
DETAIL: Key (id)=(1) is still referenced from table "b".select * from b;
id
----
1
(1 row)Changing the example to specify ON UPDATE CASCADE:
drop table a, b;
create table a (id serial, primary key (id)) partition by range (id);
create table b (id serial, primary key (id)) partition by range (id);
alter table b add constraint a_fk foreign key (id) references a (id)
on delete cascade;
Oops, I copy-pasted the wrong block of text from my terminal. I meant:
alter table b add constraint a_fk foreign key (id) references a (id)
on delete cascade on update cascade;
create table a1 partition of a for values from (1) to (2);
create table a2 partition of a for values from (2) to (3);
create table b1 partition of b for values from (1) to (2);
create table b2 partition of b for values from (2) to (3);
insert into a (id) values (1);
insert into b (id) values (1);-- correctly applies ON UPDATE CASCADE action
update a set id=2;
UPDATE 1select * from b;
id
----
2
(1 row)
--
Amit Langote
EDB: http://www.enterprisedb.com
Hi Amit,
thanks for the quick reply! Sadly I have been busy and the second part of your patch does no longer apply in src/include/nodes/execnodes.h:497.
I'm sorry, I should have been more careful rereading my posts. The case I meant is the one below. I checked the thread again. I can barely believe, I didn't post such an example along back then. Sorry for the confusion!
create table a (id serial, primary key (id)) partition by range (id);
create table b (id serial, primary key (id)) partition by range (id);
create table a1 partition of a for values from (1) to (2);
create table a2 partition of a for values from (2) to (3);
create table b1 partition of b for values from (1) to (2);
create table b2 partition of b for values from (2) to (3);
insert into a (id) values (1);
insert into b (id) values (1);
create or replace function del_trig_fkt()
returns trigger
language plpgsql
as $$
begin
raise notice 'Deleted!';
return old;
end;
$$;
create trigger a_del_trig after delete on a for each row execute function del_trig_fkt();
create or replace function public.upd_trig_fkt()
returns trigger
language plpgsql
as $function$
begin
raise notice 'Updated!';
return new;
end;
$function$;
create trigger a_upd_trig after update on a for each row execute function upd_trig_fkt();
update a set id=2;
To me the issue seems to have litte to do with the fact that the trigger is executed on the leaf node in itself, but rather we lack the infrastructure to track whether the tuple is removed or only rerouted.
Regards
Arne
________________________________
From: Amit Langote <amitlangote09@gmail.com>
Sent: Tuesday, December 15, 2020 4:45:19 AM
To: Arne Roland
Cc: Tomas Vondra; David G. Johnston; PostgreSQL-development
Subject: Re: a misbehavior of partition row movement (?)
On Tue, Dec 15, 2020 at 12:43 PM Amit Langote <amitlangote09@gmail.com> wrote:
Quoting your original example:
drop table a, b;
create table a (id serial, primary key (id)) partition by range (id);
create table b (id serial, primary key (id)) partition by range (id);
alter table b add constraint a_fk foreign key (id) references a (id)
on delete cascade;
create table a1 partition of a for values from (1) to (2);
create table a2 partition of a for values from (2) to (3);
create table b1 partition of b for values from (1) to (2);
create table b2 partition of b for values from (2) to (3);
insert into a (id) values (1);
insert into b (id) values (1);-- correctly errors out instead of silently performing the ON DELETE CASCADE
update a set id=2;
ERROR: update or delete on table "a" violates foreign key constraint
"a_fk" on table "b"
DETAIL: Key (id)=(1) is still referenced from table "b".select * from b;
id
----
1
(1 row)Changing the example to specify ON UPDATE CASCADE:
drop table a, b;
create table a (id serial, primary key (id)) partition by range (id);
create table b (id serial, primary key (id)) partition by range (id);
alter table b add constraint a_fk foreign key (id) references a (id)
on delete cascade;
Oops, I copy-pasted the wrong block of text from my terminal. I meant:
alter table b add constraint a_fk foreign key (id) references a (id)
on delete cascade on update cascade;
create table a1 partition of a for values from (1) to (2);
create table a2 partition of a for values from (2) to (3);
create table b1 partition of b for values from (1) to (2);
create table b2 partition of b for values from (2) to (3);
insert into a (id) values (1);
insert into b (id) values (1);-- correctly applies ON UPDATE CASCADE action
update a set id=2;
UPDATE 1select * from b;
id
----
2
(1 row)
--
Amit Langote
EDB: http://www.enterprisedb.com
Hi,
On Mon, Dec 21, 2020 at 11:30 PM Arne Roland <A.Roland@index.de> wrote:
thanks for the quick reply! Sadly I have been busy and the second part of your patch does no longer apply in src/include/nodes/execnodes.h:497.
I don't see any problem with applying the patch. Are you sure you're
applying the patch to the correct git branch? The patch is meant to
be applied to the development (master) branch.
I'm sorry, I should have been more careful rereading my posts. The case I meant is the one below. I checked the thread again. I can barely believe, I didn't post such an example along back then. Sorry for the confusion!
No worries, thanks for the follow up.
create table a (id serial, primary key (id)) partition by range (id);
create table b (id serial, primary key (id)) partition by range (id);
create table a1 partition of a for values from (1) to (2);
create table a2 partition of a for values from (2) to (3);
create table b1 partition of b for values from (1) to (2);
create table b2 partition of b for values from (2) to (3);
insert into a (id) values (1);
insert into b (id) values (1);create or replace function del_trig_fkt()
returns trigger
language plpgsql
as $$
begin
raise notice 'Deleted!';
return old;
end;
$$;
create trigger a_del_trig after delete on a for each row execute function del_trig_fkt();
create or replace function public.upd_trig_fkt()
returns trigger
language plpgsql
as $function$
begin
raise notice 'Updated!';
return new;
end;
$function$;
create trigger a_upd_trig after update on a for each row execute function upd_trig_fkt();update a set id=2;
The output for this I get with (or without) the patch is:
NOTICE: Deleted!
UPDATE 1
To me the issue seems to have litte to do with the fact that the trigger is executed on the leaf node in itself, but rather we lack the infrastructure to track whether the tuple is removed or only rerouted.
This behavior of partition key updates with regard to *user-defined*
AFTER triggers is documented:
https://www.postgresql.org/docs/current/trigger-definition.html
"As far as AFTER ROW triggers are concerned, AFTER DELETE and AFTER
INSERT triggers are applied; but AFTER UPDATE triggers are not applied
because the UPDATE has been converted to a DELETE and an INSERT."
I don't quite recall if the decision to implement it like this was
based on assuming that this is what users would like to see happen in
this case or the perceived difficulty of implementing it the other way
around, that is, of firing AFTER UPDATE triggers in this case.
As for the original issue of this thread, it happens to be fixed by
firing the *internal* AFTER UPDATE triggers that are involved in
enforcing the foreign key even if the UPDATE has been turned into
DELETE+INSERT, which it makes sense to do, because what can happen
today with CASCADE triggers does not seem like a useful behavior by
any measure.
--
Amit Langote
EDB: http://www.enterprisedb.com
On Tue, Dec 22, 2020 at 4:16 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Mon, Dec 21, 2020 at 11:30 PM Arne Roland <A.Roland@index.de> wrote:
thanks for the quick reply! Sadly I have been busy and the second part of your patch does no longer apply in src/include/nodes/execnodes.h:497.
I don't see any problem with applying the patch. Are you sure you're
applying the patch to the correct git branch? The patch is meant to
be applied to the development (master) branch.
Sorry, it seems you are right and the 2nd patch indeed fails to apply to master.
Attached updated version.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v2-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v2-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From 91975a0863bac88fd0b467c0625bc52920678215 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v2 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 354 ++++++++++++++++++++-----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 20 +-
src/bin/psql/describe.c | 6 +-
src/include/commands/trigger.h | 5 +-
src/test/regress/expected/triggers.out | 4 +-
6 files changed, 407 insertions(+), 83 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1fa9f19f08..ddd40ef63f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -449,12 +449,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -462,15 +464,34 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
+static void AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -8761,7 +8782,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8774,7 +8796,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -8814,7 +8837,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -8822,6 +8846,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -8921,12 +8947,11 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* If the referenced table is a plain relation, create the action triggers
* that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -8970,7 +8995,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9019,8 +9045,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9034,14 +9064,16 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9070,6 +9102,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9111,7 +9152,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9190,10 +9234,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9249,6 +9297,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9278,6 +9327,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9295,6 +9352,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9361,6 +9420,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ GetForeignKeyActionTriggers(constrForm, trigrel,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9373,11 +9436,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9400,6 +9467,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9421,6 +9489,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9450,6 +9526,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9478,6 +9556,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9496,7 +9578,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9595,9 +9680,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9618,13 +9707,15 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
@@ -9688,12 +9779,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -9726,13 +9815,135 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
+
+ AttachForeignKeyCheckTriggers(fk, partRelid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
CommandCounterIncrement();
return true;
}
+static void
+GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(constrForm->oid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != constrForm->conrelid)
+ continue;
+ if (trgform->tgrelid != constrForm->confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key constraint %u",
+ constrForm->oid);
+
+ systable_endscan(scan);
+}
+
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+static void
+AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+}
+
+static void
+DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid)
+{
+ Relation trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, partRelid);
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, partRelid);
+
+ table_close(trigrel, RowExclusiveLock);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10533,10 +10744,12 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10576,11 +10789,14 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
@@ -10590,9 +10806,12 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10644,9 +10863,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10701,9 +10923,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
@@ -10714,12 +10939,16 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -16818,20 +17047,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
if (!TRIGGER_FOR_ROW(trigForm->tgtype))
continue;
- /*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
- */
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ /* Don't clone internal triggers. They are dealt with separately. */
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17080,7 +17297,11 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
+
+ DetachForeignKeyCheckTriggers(fk, RelationGetRelid(partRel));
ReleaseSysCache(contup);
}
@@ -17156,6 +17377,13 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys. They will be dealt with separately.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c336b238aa..7b2d0de987 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -136,8 +136,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -188,6 +190,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -726,6 +729,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -752,17 +756,16 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -859,7 +862,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1228,6 +1231,82 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ Assert(trigForm->tgparentid == 0);
+ if (trigForm->tgparentid != InvalidOid)
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8b1e5cc2b5..3b233a9411 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7958,7 +7958,25 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 90000)
+ if (fout->remoteVersion >= 130000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ */
+ appendPQExpBuffer(query,
+ "SELECT tgname, "
+ "tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(oid, false) AS tgdef, "
+ "tgenabled, tableoid, oid "
+ "FROM pg_catalog.pg_trigger t "
+ "WHERE tgrelid = '%u'::pg_catalog.oid "
+ "AND NOT tgisinternal "
+ "AND tgparentid = 0",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 90000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 14150d05a9..1be3843105 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2973,11 +2973,7 @@ describeOneTableDetails(const char *schemaname,
" 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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
- else if (pset.sversion >= 90000)
+ if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
else if (pset.sversion >= 80300)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 244540d90b..f7f0de48c6 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -154,7 +154,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
-
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 1dc525251a..4051b6fec1 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3216,7 +3216,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v2-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v2-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From 7c40985b8bd6995697abda48aa396b14960a926b Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v2 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned primary key table referenced in a
foreign key constraint causes a row to move from one partition to
another, instead of firing an UPDATE after-trigger event, DELETE and
INSERT events are fired on the source and the destination leaf
partition, respectively, which can result in pretty surprising
outcomes. To be fair, it would be wrong to fire the UPDATE event
on the source leaf partition itself, because the new row is not
inserted into it.
This commit teaches trigger.c to skip queuing the aforementioned
DELETE and INSERT events on the leaf partitions in favor of an
UPDATE event fired on the "root" target relation, which makes sense
because both the old and new tuple "logically" belong to it. To
make that possible, this adjusts AFTER trigger data strucutures to
allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. The use case of distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root partitioned table is perhaps of minor importance.
---
src/backend/commands/trigger.c | 162 +++++++++++------
src/backend/executor/execMain.c | 9 +
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 212 ++++++++++++++++++++--
src/backend/utils/adt/ri_triggers.c | 17 +-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 4 +
src/test/regress/expected/foreign_key.out | 146 ++++++++++++++-
src/test/regress/sql/foreign_key.sql | 84 +++++++++
9 files changed, 569 insertions(+), 71 deletions(-)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7b2d0de987..58277bef0d 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -98,7 +98,9 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2317,7 +2319,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2406,7 +2408,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2531,7 +2533,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2628,7 +2630,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2651,7 +2654,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2766,7 +2769,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
GetAllUpdatedColumns(relinfo, estate),
transition_capture);
@@ -2913,7 +2916,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2947,7 +2951,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else if (fdw_trigtuple != NULL)
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
GetAllUpdatedColumns(relinfo, estate),
transition_capture);
@@ -3073,7 +3077,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3361,19 +3365,21 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. For partitioned tables, we never
+ * queue any events for its deferred triggers. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3394,8 +3400,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3589,7 +3595,8 @@ typedef struct AfterTriggersData
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3638,15 +3645,15 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
+ * Get the tuplestore for the current trigger query level, creating it
* if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetCurrentAfterTriggerTuplestore(void)
{
Tuplestorestate *ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
+ ret = afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
if (ret == NULL)
{
MemoryContext oldcxt;
@@ -3665,7 +3672,7 @@ GetCurrentFDWTuplestore(void)
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
+ afterTriggers.query_stack[afterTriggers.query_depth].tuplestore = ret;
}
return ret;
@@ -3999,22 +4006,22 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore = GetCurrentAfterTriggerTuplestore();
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4315,7 +4322,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4699,8 +4707,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5032,7 +5040,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5503,7 +5511,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5517,7 +5526,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5715,7 +5724,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5735,16 +5746,62 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ /*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ mtstate->rootResultRelInfo->ri_RelationDesc->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)
+ {
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ continue;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+ break;
+ }
+ }
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ if (tuplestore == NULL)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ tuplestore = GetCurrentAfterTriggerTuplestore();
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5818,16 +5875,17 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(tuplestore, newslot);
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 7179f589f9..14f7680277 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1438,8 +1438,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 01d26881e7..3f5375416c 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index ab3d655e60..a7b4f6936f 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -42,6 +42,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -380,7 +381,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -657,7 +660,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -695,6 +698,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -989,7 +997,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1004,7 +1012,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1076,7 +1084,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
@@ -1165,8 +1175,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1179,6 +1190,96 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1332,9 +1433,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1346,14 +1450,97 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * Enforce foreign key actions using the root relation's triggers.
+ * NULL insert_destrel means that the move failed to occur or that
+ * the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ {
+ ListCell *lc;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(resultRelInfo,
+ mtstate);
+
+ /*
+ * There better not be any foreign keys that point into some
+ * non-root ancestor but not root (tgisclone is false), because
+ * we can't enforce them (for now) like we can those on the
+ * root parent.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted tuple ("new" tuple) into the root table's
+ * slot, possibly converting it.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(resultRelInfo, mtstate);
+ if (inserted_tuple != slot && map)
+ slot = execute_attr_map_slot(map->attrMap, inserted_tuple,
+ rootslot);
+ else
+ slot = ExecCopySlot(rootslot, inserted_tuple);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(resultRelationDesc,
+ tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ slot, NIL, NULL);
+ }
+
+ return returning_slot;
}
/*
@@ -1519,7 +1706,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2124,7 +2312,7 @@ ExecModifyTable(PlanState *pstate)
{
case CMD_INSERT:
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 5ab134a853..92c17dacdc 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1259,11 +1259,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index f7f0de48c6..06bb3ba3ed 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -205,6 +205,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -224,6 +225,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 61ba4c3666..28e05b4cc2 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -497,9 +497,13 @@ typedef struct ResultRelInfo
* transition tuple capture or update partition row movement is active.
*/
TupleConversionMap *ri_ChildToRootMap;
+ bool ri_ChildToRootMapValid;
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 07bd5b6434..e2c2c3d756 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2407,7 +2407,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2470,3 +2470,147 @@ DROP SCHEMA fkpart9 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart9.pk
drop cascades to table fkpart9.fk
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart10.pk12 | 2
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 2
+ fkpart10.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 2
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 2
+ fkpart10.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+ tableoid | a
+---------------+---
+ fkpart10.pk11 | 1
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 1
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 4
+ fkpart10.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart10.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart10 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart10.pk
+drop cascades to table fkpart10.fk_parted
+drop cascades to table fkpart10.fk_another
+drop cascades to function fkpart10.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index c5c9011afc..5902b787c5 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1738,3 +1738,87 @@ DELETE FROM fkpart9.pk WHERE a=35;
SELECT * FROM fkpart9.pk;
SELECT * FROM fkpart9.fk;
DROP SCHEMA fkpart9 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+DROP TABLE fkpart10.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart10 CASCADE;
--
2.24.1
While I'd agree that simply deleting with "on delete cascade" on tuple rerouting is a strong enough violation of the spirit of partitioning changing that behavior, I am surprised by the idea to make a differentiation between fks and other triggers. The way user defined triggers work, is a violation to the same degree I get complaints about that on a monthly (if not weekly) basis. It's easy to point towards the docs, but the docs offer no solution to archive the behavior, that makes partitioning somewhat transparent. Most people I know don't partition because they like to complicate things, but just because they have to much data.
It isn't just a thing with after triggers. Imo before triggers are even worse: If we move a row between partitions we fire all three triggers at once (at different leaf pages obviously). It's easy to point towards the docs. Heart bleed was well documented, but I am happy that it was fixed. I don't want to imply this totally unrelated security issue has anything to do with our weird behavior. I just want to raise the question whether that's a good thing, because frankly I haven't met anyone thus far, who thought it is.
To me it seems the RI triggers are just a specific victim of the way we made triggers work in general.
What I tried to express, albeit I apparently failed: I care about having triggers, which make partitioning transparent, on the long run.
because what can happen
today with CASCADE triggers does not seem like a useful behavior by
any measure.
I wholeheartedly agree. I just want to point out, that you could state the same about triggers in general.
Backwards compatibility is usually a pretty compelling argument. I would still want to raise the question, whether this should change, because I frankly think this is a bad idea.
You might ask: Why am I raising this question in this thread?
In case we can not (instantly) agree on the fact that this behavior should change, I'd still prefer to think about making that behavior possible for other triggers (possibly later) as well. One possibility would be having an entry in the catalog to mark when the trigger should fire.
I don't want to stall your definitely useful RI-Trigger fix, but I sincerely believe, that we have to do better with triggers in general.
If we would make the condition when to fire or not dependent something besides that fact whether the trigger is internal, we could at a later date choose to make both ways available, if anyone makes a good case for this. Even though I still think it's not worth it.
I don't quite recall if the decision to implement it like this was
based on assuming that this is what users would like to see happen in
this case or the perceived difficulty of implementing it the other way
around, that is, of firing AFTER UPDATE triggers in this case.
I tried to look that up, but I couldn't find any discussion about this. Do you have any ideas in which thread that was handled?
Sorry, it seems you are right and the 2nd patch indeed fails to apply to master.
Thank you! I hope to have a more in depth look later this week.
Regards
Arne
Hi,
On Mon, Dec 28, 2020 at 10:01 PM Arne Roland <A.Roland@index.de> wrote:
While I'd agree that simply deleting with "on delete cascade" on tuple rerouting is a strong enough violation of the spirit of partitioning changing that behavior, I am surprised by the idea to make a differentiation between fks and other triggers. The way user defined triggers work, is a violation to the same degree I get complaints about that on a monthly (if not weekly) basis. It's easy to point towards the docs, but the docs offer no solution to archive the behavior, that makes partitioning somewhat transparent. Most people I know don't partition because they like to complicate things, but just because they have to much data.
It isn't just a thing with after triggers. Imo before triggers are even worse: If we move a row between partitions we fire all three triggers at once (at different leaf pages obviously). It's easy to point towards the docs. Heart bleed was well documented, but I am happy that it was fixed. I don't want to imply this totally unrelated security issue has anything to do with our weird behavior. I just want to raise the question whether that's a good thing, because frankly I haven't met anyone thus far, who thought it is.
Just to be clear, are you only dissatisfied with the firing of
triggers during a row-moving UPDATEs on partitioned tables or all
firing behaviors of triggers defined on partitioned tables? Can you
give a specific example of the odd behavior in that case?
To me it seems the RI triggers are just a specific victim of the way we made triggers work in general.
That's true.
What I tried to express, albeit I apparently failed: I care about having triggers, which make partitioning transparent, on the long run.
because what can happen
today with CASCADE triggers does not seem like a useful behavior by
any measure.I wholeheartedly agree. I just want to point out, that you could state the same about triggers in general.
Backwards compatibility is usually a pretty compelling argument. I would still want to raise the question, whether this should change, because I frankly think this is a bad idea.
You might ask: Why am I raising this question in this thread?
In case we can not (instantly) agree on the fact that this behavior should change, I'd still prefer to think about making that behavior possible for other triggers (possibly later) as well. One possibility would be having an entry in the catalog to mark when the trigger should fire.
Sorry, I don't understand what new setting for triggers you are
thinking of here.
I don't want to stall your definitely useful RI-Trigger fix, but I sincerely believe, that we have to do better with triggers in general.
If we would make the condition when to fire or not dependent something besides that fact whether the trigger is internal, we could at a later date choose to make both ways available, if anyone makes a good case for this. Even though I still think it's not worth it.
I don't quite recall if the decision to implement it like this was
based on assuming that this is what users would like to see happen in
this case or the perceived difficulty of implementing it the other way
around, that is, of firing AFTER UPDATE triggers in this case.I tried to look that up, but I couldn't find any discussion about this. Do you have any ideas in which thread that was handled?
It was discussed here:
/messages/by-id/CAJ3gD9do9o2ccQ7j7+tSgiE1REY65XRiMb=yJO3u3QhyP8EEPQ@mail.gmail.com
It's a huge discussion, so you'll have to ctrl+f "trigger" to spot
relevant emails. You might notice that the developers who
participated in that discussion gave various opinions and what we have
today got there as a result of a majority of them voting for the
current approach. Someone also said this during the discussion:
"Regarding the trigger issue, I can't claim to have a terribly strong
opinion on this. I think that practically anything we do here might
upset somebody, but probably any halfway-reasonable thing we choose to
do will be OK for most people." So what we've got is that
"halfway-reasonable" thing, YMMV. :)
--
Amit Langote
EDB: http://www.enterprisedb.com
On 2021-01-08 09:54, Amit Langote wrote:
I don't quite recall if the decision to implement it like this was
based on assuming that this is what users would like to see happen in
this case or the perceived difficulty of implementing it the other way
around, that is, of firing AFTER UPDATE triggers in this case.I tried to look that up, but I couldn't find any discussion about this. Do you have any ideas in which thread that was handled?
It was discussed here:
/messages/by-id/CAJ3gD9do9o2ccQ7j7+tSgiE1REY65XRiMb=yJO3u3QhyP8EEPQ@mail.gmail.com
It's a huge discussion, so you'll have to ctrl+f "trigger" to spot
relevant emails. You might notice that the developers who
participated in that discussion gave various opinions and what we have
today got there as a result of a majority of them voting for the
current approach. Someone also said this during the discussion:
"Regarding the trigger issue, I can't claim to have a terribly strong
opinion on this. I think that practically anything we do here might
upset somebody, but probably any halfway-reasonable thing we choose to
do will be OK for most people." So what we've got is that
"halfway-reasonable" thing, YMMV. :)
Could you summarize here what you are trying to do with respect to what
was decided before? I'm a bit confused, looking through the patches you
have posted. The first patch you posted hard-coded FK trigger OIDs
specifically, other patches talk about foreign key triggers in general
or special case internal triggers or talk about all triggers.
On Wed, Jan 20, 2021 at 4:13 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:
On 2021-01-08 09:54, Amit Langote wrote:
I don't quite recall if the decision to implement it like this was
based on assuming that this is what users would like to see happen in
this case or the perceived difficulty of implementing it the other way
around, that is, of firing AFTER UPDATE triggers in this case.I tried to look that up, but I couldn't find any discussion about this. Do you have any ideas in which thread that was handled?
It was discussed here:
/messages/by-id/CAJ3gD9do9o2ccQ7j7+tSgiE1REY65XRiMb=yJO3u3QhyP8EEPQ@mail.gmail.com
It's a huge discussion, so you'll have to ctrl+f "trigger" to spot
relevant emails. You might notice that the developers who
participated in that discussion gave various opinions and what we have
today got there as a result of a majority of them voting for the
current approach. Someone also said this during the discussion:
"Regarding the trigger issue, I can't claim to have a terribly strong
opinion on this. I think that practically anything we do here might
upset somebody, but probably any halfway-reasonable thing we choose to
do will be OK for most people." So what we've got is that
"halfway-reasonable" thing, YMMV. :)Could you summarize here what you are trying to do with respect to what
was decided before? I'm a bit confused, looking through the patches you
have posted. The first patch you posted hard-coded FK trigger OIDs
specifically, other patches talk about foreign key triggers in general
or special case internal triggers or talk about all triggers.
The original problem statement is this: the way we generally fire
row-level triggers of a partitioned table can lead to some unexpected
behaviors of the foreign keys pointing to that partitioned table
during its cross-partition updates.
Let's start with an example that shows how triggers are fired during a
cross-partition update:
create table p (a numeric primary key) partition by list (a);
create table p1 partition of p for values in (1);
create table p2 partition of p for values in (2);
create or replace function report_trig_details() returns trigger as $$
begin raise notice '% % on %', tg_when, tg_op, tg_relname; if tg_op =
'DELETE' then return old; end if; return new; end; $$ language
plpgsql;
create trigger trig1 before update on p for each row execute function
report_trig_details();
create trigger trig2 after update on p for each row execute function
report_trig_details();
create trigger trig3 before delete on p for each row execute function
report_trig_details();
create trigger trig4 after delete on p for each row execute function
report_trig_details();
create trigger trig5 before insert on p for each row execute function
report_trig_details();
create trigger trig6 after insert on p for each row execute function
report_trig_details();
insert into p values (1);
update p set a = 2;
NOTICE: BEFORE UPDATE on p1
NOTICE: BEFORE DELETE on p1
NOTICE: BEFORE INSERT on p2
NOTICE: AFTER DELETE on p1
NOTICE: AFTER INSERT on p2
UPDATE 1
(AR update triggers are not fired.)
For contrast, here is an intra-partition update:
update p set a = a;
NOTICE: BEFORE UPDATE on p2
NOTICE: AFTER UPDATE on p2
UPDATE 1
Now, the trigger machinery makes no distinction between user-defined
and internal triggers, which has implications for the foreign key
enforcing triggers on partitions. Consider the following example:
create table q (a bigint references p);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
ERROR: update or delete on table "p2" violates foreign key constraint
"q_a_fkey2" on table "q"
DETAIL: Key (a)=(2) is still referenced from table "q".
So the RI delete trigger (NOT update) on p2 prevents the DELETE that
occurs as part of the row movement. One might make the updates
cascade and expect that to prevent the error:
drop table q;
create table q (a bigint references p on update cascade);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
ERROR: update or delete on table "p2" violates foreign key constraint
"q_a_fkey2" on table "q"
DETAIL: Key (a)=(2) is still referenced from table "q".
No luck, because again it's the RI delete trigger on p2 that gets
fired. If you make deletes cascade too, an even worse thing happens:
drop table q;
create table q (a bigint references p on update cascade on delete cascade);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
NOTICE: AFTER DELETE on p2
NOTICE: AFTER INSERT on p1
UPDATE 1
select * from q;
a
---
(0 rows)
The RI delete trigger deleted 2 from q, whereas the expected result
would've been for q to be updated to change 2 to 1.
This shows that the way we've made these triggers behave in general
can cause some unintended behaviors for foreign keys during
cross-partition updates. I started this thread to do something about
that and sent a patch to prevent cross-partition updates at all when
there are foreign keys pointing to it. As others pointed out, that's
not a great long-term solution to the problem, but that's what we may
have to do in the back-branches if anything at all.
So I wrote another patch targeting the dev branch to make the
cross-partition updates work while producing a sane foreign key
behavior. The idea of the patch is to tweak the firing of AFTER
triggers such that unintended RI triggers don't get fired, that is,
those corresponding to DELETE and INSERT occurring internally as part
of a cross-partition update. Instead we now fire the AFTER UPDATE
triggers, passing the root table as the target result relation (not
the source partition because the new row doesn't belong to it). This
results in the same foreign key behavior as when no partitioning is
involved at all.
Then, Arne came along and suggested that we do this kind of firing for
*all* triggers, not just the internal RI triggers, or at least that's
what I understood Arne as saying. That however would be changing the
original design of cross-partition updates and will change the
documented user-visible trigger behavior. Changing this for internal
triggers like the patch does changes no user-visible behavior, AFAIK,
other than fixing the foreign key annoyance. So I said if we do want
to go the way that Arne is suggesting, it should be its own discussion
and that's that.
Sorry for a long "summary", but I hope it helps clarify things somewhat.
--
Amit Langote
EDB: http://www.enterprisedb.com
On Wed, Jan 20, 2021 at 7:03 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Wed, Jan 20, 2021 at 4:13 PM Peter Eisentraut
Could you summarize here what you are trying to do with respect to what
was decided before? I'm a bit confused, looking through the patches you
have posted. The first patch you posted hard-coded FK trigger OIDs
specifically, other patches talk about foreign key triggers in general
or special case internal triggers or talk about all triggers.The original problem statement is this: the way we generally fire
row-level triggers of a partitioned table can lead to some unexpected
behaviors of the foreign keys pointing to that partitioned table
during its cross-partition updates.Let's start with an example that shows how triggers are fired during a
cross-partition update:create table p (a numeric primary key) partition by list (a);
create table p1 partition of p for values in (1);
create table p2 partition of p for values in (2);
create or replace function report_trig_details() returns trigger as $$
begin raise notice '% % on %', tg_when, tg_op, tg_relname; if tg_op =
'DELETE' then return old; end if; return new; end; $$ language
plpgsql;
create trigger trig1 before update on p for each row execute function
report_trig_details();
create trigger trig2 after update on p for each row execute function
report_trig_details();
create trigger trig3 before delete on p for each row execute function
report_trig_details();
create trigger trig4 after delete on p for each row execute function
report_trig_details();
create trigger trig5 before insert on p for each row execute function
report_trig_details();
create trigger trig6 after insert on p for each row execute function
report_trig_details();insert into p values (1);
update p set a = 2;
NOTICE: BEFORE UPDATE on p1
NOTICE: BEFORE DELETE on p1
NOTICE: BEFORE INSERT on p2
NOTICE: AFTER DELETE on p1
NOTICE: AFTER INSERT on p2
UPDATE 1(AR update triggers are not fired.)
For contrast, here is an intra-partition update:
update p set a = a;
NOTICE: BEFORE UPDATE on p2
NOTICE: AFTER UPDATE on p2
UPDATE 1Now, the trigger machinery makes no distinction between user-defined
and internal triggers, which has implications for the foreign key
enforcing triggers on partitions. Consider the following example:create table q (a bigint references p);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
ERROR: update or delete on table "p2" violates foreign key constraint
"q_a_fkey2" on table "q"
DETAIL: Key (a)=(2) is still referenced from table "q".So the RI delete trigger (NOT update) on p2 prevents the DELETE that
occurs as part of the row movement. One might make the updates
cascade and expect that to prevent the error:drop table q;
create table q (a bigint references p on update cascade);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
ERROR: update or delete on table "p2" violates foreign key constraint
"q_a_fkey2" on table "q"
DETAIL: Key (a)=(2) is still referenced from table "q".No luck, because again it's the RI delete trigger on p2 that gets
fired. If you make deletes cascade too, an even worse thing happens:drop table q;
create table q (a bigint references p on update cascade on delete cascade);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
NOTICE: AFTER DELETE on p2
NOTICE: AFTER INSERT on p1
UPDATE 1
select * from q;
a
---
(0 rows)The RI delete trigger deleted 2 from q, whereas the expected result
would've been for q to be updated to change 2 to 1.This shows that the way we've made these triggers behave in general
can cause some unintended behaviors for foreign keys during
cross-partition updates. I started this thread to do something about
that and sent a patch to prevent cross-partition updates at all when
there are foreign keys pointing to it. As others pointed out, that's
not a great long-term solution to the problem, but that's what we may
have to do in the back-branches if anything at all.So I wrote another patch targeting the dev branch to make the
cross-partition updates work while producing a sane foreign key
behavior. The idea of the patch is to tweak the firing of AFTER
triggers such that unintended RI triggers don't get fired, that is,
those corresponding to DELETE and INSERT occurring internally as part
of a cross-partition update. Instead we now fire the AFTER UPDATE
triggers, passing the root table as the target result relation (not
the source partition because the new row doesn't belong to it). This
results in the same foreign key behavior as when no partitioning is
involved at all.Then, Arne came along and suggested that we do this kind of firing for
*all* triggers, not just the internal RI triggers, or at least that's
what I understood Arne as saying. That however would be changing the
original design of cross-partition updates and will change the
documented user-visible trigger behavior. Changing this for internal
triggers like the patch does changes no user-visible behavior, AFAIK,
other than fixing the foreign key annoyance. So I said if we do want
to go the way that Arne is suggesting, it should be its own discussion
and that's that.Sorry for a long "summary", but I hope it helps clarify things somewhat.
Here is an updated version of the patch with some cosmetic changes
from the previous version. I moved the code being added to
AfterTriggerSaveEvent() and ExecUpdate() into separate subroutines to
improve readability, hopefully.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v3-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v3-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From b05c34a1289ed34e1350be8be4e969cb793fe106 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v3 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 354 ++++++++++++++++++++-----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 20 +-
src/bin/psql/describe.c | 6 +-
src/include/commands/trigger.h | 5 +-
src/test/regress/expected/triggers.out | 4 +-
6 files changed, 407 insertions(+), 83 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8687e9a97c..4e8929adea 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -449,12 +449,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -462,15 +464,34 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
+static void AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -8763,7 +8784,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8776,7 +8798,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -8816,7 +8839,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -8824,6 +8848,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -8923,12 +8949,11 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* If the referenced table is a plain relation, create the action triggers
* that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -8972,7 +8997,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9021,8 +9047,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9036,14 +9066,16 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9072,6 +9104,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9113,7 +9154,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9192,10 +9236,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9251,6 +9299,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9280,6 +9329,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9297,6 +9354,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9363,6 +9422,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ GetForeignKeyActionTriggers(constrForm, trigrel,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9375,11 +9438,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9402,6 +9469,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9423,6 +9491,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9452,6 +9528,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9480,6 +9558,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9498,7 +9580,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9597,9 +9682,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9620,13 +9709,15 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
@@ -9690,12 +9781,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -9728,13 +9817,135 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
+
+ AttachForeignKeyCheckTriggers(fk, partRelid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
CommandCounterIncrement();
return true;
}
+static void
+GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(constrForm->oid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != constrForm->conrelid)
+ continue;
+ if (trgform->tgrelid != constrForm->confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key constraint %u",
+ constrForm->oid);
+
+ systable_endscan(scan);
+}
+
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+static void
+AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+}
+
+static void
+DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid)
+{
+ Relation trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, partRelid);
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, partRelid);
+
+ table_close(trigrel, RowExclusiveLock);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10535,10 +10746,12 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10578,11 +10791,14 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
@@ -10592,9 +10808,12 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10646,9 +10865,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10703,9 +10925,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
@@ -10716,12 +10941,16 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -16824,20 +17053,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
if (!TRIGGER_FOR_ROW(trigForm->tgtype))
continue;
- /*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
- */
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ /* Don't clone internal triggers. They are dealt with separately. */
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17086,7 +17303,11 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
+
+ DetachForeignKeyCheckTriggers(fk, RelationGetRelid(partRel));
ReleaseSysCache(contup);
}
@@ -17162,6 +17383,13 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys. They will be dealt with separately.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 3e7086c5e5..6bf1bba4cd 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -134,8 +134,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -186,6 +188,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -724,6 +727,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -750,17 +754,16 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -857,7 +860,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1226,6 +1229,82 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ Assert(trigForm->tgparentid == 0);
+ if (trigForm->tgparentid != InvalidOid)
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 798d14580e..6333d89318 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7961,7 +7961,25 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 90000)
+ if (fout->remoteVersion >= 130000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ */
+ appendPQExpBuffer(query,
+ "SELECT tgname, "
+ "tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(oid, false) AS tgdef, "
+ "tgenabled, tableoid, oid "
+ "FROM pg_catalog.pg_trigger t "
+ "WHERE tgrelid = '%u'::pg_catalog.oid "
+ "AND NOT tgisinternal "
+ "AND tgparentid = 0",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 90000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 20af5a92b4..a695f954b8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2973,11 +2973,7 @@ describeOneTableDetails(const char *schemaname,
" 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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
- else if (pset.sversion >= 90000)
+ if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
else if (pset.sversion >= 80300)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..c4f742773c 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -154,7 +154,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
-
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 1dc525251a..4051b6fec1 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3216,7 +3216,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v3-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v3-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From 382d8112d5753342c2310552681212913e5d8dc8 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v3 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing of the delete triggers that implement those foreign keys can
result in surprising outcomes for those keys. For example, a given
foreign key's delete trigger which implements the ON DELETE CASCADE
clause of that key will delete any referencing rows, although it
should not, because the referenced row is simply being moved into
another partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
To make this possible, this adjusts AFTER trigger data strucutures
to allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
src/backend/commands/trigger.c | 177 +++++++++++-----
src/backend/executor/execMain.c | 9 +
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 237 ++++++++++++++++++++--
src/backend/utils/adt/ri_triggers.c | 17 +-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 4 +
src/test/regress/expected/foreign_key.out | 146 ++++++++++++-
src/test/regress/sql/foreign_key.sql | 84 ++++++++
9 files changed, 609 insertions(+), 71 deletions(-)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6bf1bba4cd..41d8111535 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -96,13 +96,18 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static bool SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel);
/*
@@ -2315,7 +2320,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2404,7 +2409,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2529,7 +2534,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2626,7 +2631,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2649,7 +2655,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2764,7 +2770,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
GetAllUpdatedColumns(relinfo, estate),
transition_capture);
@@ -2911,7 +2917,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2945,7 +2952,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else if (fdw_trigtuple != NULL)
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
GetAllUpdatedColumns(relinfo, estate),
transition_capture);
@@ -3071,7 +3078,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3359,19 +3366,21 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. For partitioned tables, we never
+ * queue any events for its deferred triggers. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3392,8 +3401,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3587,7 +3596,8 @@ typedef struct AfterTriggersData
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3636,15 +3646,15 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
+ * Get the tuplestore for the current trigger query level, creating it
* if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetCurrentAfterTriggerTuplestore(void)
{
Tuplestorestate *ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
+ ret = afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
if (ret == NULL)
{
MemoryContext oldcxt;
@@ -3663,7 +3673,7 @@ GetCurrentFDWTuplestore(void)
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
+ afterTriggers.query_stack[afterTriggers.query_depth].tuplestore = ret;
}
return ret;
@@ -3997,22 +4007,22 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore = GetCurrentAfterTriggerTuplestore();
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4313,7 +4323,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4697,8 +4708,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5030,7 +5041,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5501,7 +5512,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5515,7 +5527,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5713,7 +5725,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5733,16 +5747,22 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ SkipCrossPartitionUpdateFKeyTrigger(mtstate, trigger, event, rel))
+ continue;
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ if (tuplestore == NULL)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ tuplestore = GetCurrentAfterTriggerTuplestore();
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5816,17 +5836,70 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(tuplestore, newslot);
+ }
+}
+
+/*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+static bool
+SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel)
+{
+ Relation rootRelDesc = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (rootRelDesc->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ return false;
+
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ return true;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ return true;
+ break;
}
+
+ return false;
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index f4dd47acc7..ec8505402c 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1438,8 +1438,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 1e285e0349..9ae702c5cb 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 5d90337498..c357581d0b 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -42,6 +42,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -387,7 +388,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -714,7 +717,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -752,6 +755,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1110,7 +1118,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1125,7 +1133,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1197,7 +1205,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
@@ -1286,8 +1296,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1300,6 +1311,180 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up trigger events necessary to check that a cross-partition update
+ * of the target partitioned table hasn't broken any foreign keys pointing
+ * to it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ HeapTuple oldtuple;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ Relation sourcePartRelDesc = sourcePartInfo->ri_RelationDesc;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo, mtstate);
+
+ /*
+ * There better not be any foreign keys that point directly to a non-root
+ * ancestor of the target source partition, because we can't enforce them.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted "new" tuple into the root table's slot, after
+ * converting it if needed.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(sourcePartInfo, mtstate);
+ if (newslot != oldslot && map)
+ newslot = execute_attr_map_slot(map->attrMap, newslot, rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(sourcePartRelDesc, tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1453,9 +1638,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1467,14 +1655,38 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1640,7 +1852,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2248,7 +2461,7 @@ ExecModifyTable(PlanState *pstate)
{
case CMD_INSERT:
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 6e3a41062f..555f204fd9 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1259,11 +1259,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index c4f742773c..79b4ba129d 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -205,6 +205,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -224,6 +225,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index d65099c94a..919a7f3f63 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -503,9 +503,13 @@ typedef struct ResultRelInfo
* transition tuple capture or update partition row movement is active.
*/
TupleConversionMap *ri_ChildToRootMap;
+ bool ri_ChildToRootMapValid;
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 07bd5b6434..e2c2c3d756 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2407,7 +2407,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2470,3 +2470,147 @@ DROP SCHEMA fkpart9 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart9.pk
drop cascades to table fkpart9.fk
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart10.pk12 | 2
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 2
+ fkpart10.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 2
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 2
+ fkpart10.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+ tableoid | a
+---------------+---
+ fkpart10.pk11 | 1
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 1
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 4
+ fkpart10.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart10.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart10 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart10.pk
+drop cascades to table fkpart10.fk_parted
+drop cascades to table fkpart10.fk_another
+drop cascades to function fkpart10.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index c5c9011afc..5902b787c5 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1738,3 +1738,87 @@ DELETE FROM fkpart9.pk WHERE a=35;
SELECT * FROM fkpart9.pk;
SELECT * FROM fkpart9.fk;
DROP SCHEMA fkpart9 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+DROP TABLE fkpart10.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart10 CASCADE;
--
2.24.1
On Wed, Jan 20, 2021 at 7:04 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Wed, Jan 20, 2021 at 4:13 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:On 2021-01-08 09:54, Amit Langote wrote:
I don't quite recall if the decision to implement it like this was
based on assuming that this is what users would like to see happen in
this case or the perceived difficulty of implementing it the other way
around, that is, of firing AFTER UPDATE triggers in this case.I tried to look that up, but I couldn't find any discussion about this. Do you have any ideas in which thread that was handled?
It was discussed here:
/messages/by-id/CAJ3gD9do9o2ccQ7j7+tSgiE1REY65XRiMb=yJO3u3QhyP8EEPQ@mail.gmail.com
It's a huge discussion, so you'll have to ctrl+f "trigger" to spot
relevant emails. You might notice that the developers who
participated in that discussion gave various opinions and what we have
today got there as a result of a majority of them voting for the
current approach. Someone also said this during the discussion:
"Regarding the trigger issue, I can't claim to have a terribly strong
opinion on this. I think that practically anything we do here might
upset somebody, but probably any halfway-reasonable thing we choose to
do will be OK for most people." So what we've got is that
"halfway-reasonable" thing, YMMV. :)Could you summarize here what you are trying to do with respect to what
was decided before? I'm a bit confused, looking through the patches you
have posted. The first patch you posted hard-coded FK trigger OIDs
specifically, other patches talk about foreign key triggers in general
or special case internal triggers or talk about all triggers.The original problem statement is this: the way we generally fire
row-level triggers of a partitioned table can lead to some unexpected
behaviors of the foreign keys pointing to that partitioned table
during its cross-partition updates.Let's start with an example that shows how triggers are fired during a
cross-partition update:create table p (a numeric primary key) partition by list (a);
create table p1 partition of p for values in (1);
create table p2 partition of p for values in (2);
create or replace function report_trig_details() returns trigger as $$
begin raise notice '% % on %', tg_when, tg_op, tg_relname; if tg_op =
'DELETE' then return old; end if; return new; end; $$ language
plpgsql;
create trigger trig1 before update on p for each row execute function
report_trig_details();
create trigger trig2 after update on p for each row execute function
report_trig_details();
create trigger trig3 before delete on p for each row execute function
report_trig_details();
create trigger trig4 after delete on p for each row execute function
report_trig_details();
create trigger trig5 before insert on p for each row execute function
report_trig_details();
create trigger trig6 after insert on p for each row execute function
report_trig_details();insert into p values (1);
update p set a = 2;
NOTICE: BEFORE UPDATE on p1
NOTICE: BEFORE DELETE on p1
NOTICE: BEFORE INSERT on p2
NOTICE: AFTER DELETE on p1
NOTICE: AFTER INSERT on p2
UPDATE 1(AR update triggers are not fired.)
For contrast, here is an intra-partition update:
update p set a = a;
NOTICE: BEFORE UPDATE on p2
NOTICE: AFTER UPDATE on p2
UPDATE 1Now, the trigger machinery makes no distinction between user-defined
and internal triggers, which has implications for the foreign key
enforcing triggers on partitions. Consider the following example:create table q (a bigint references p);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
ERROR: update or delete on table "p2" violates foreign key constraint
"q_a_fkey2" on table "q"
DETAIL: Key (a)=(2) is still referenced from table "q".So the RI delete trigger (NOT update) on p2 prevents the DELETE that
occurs as part of the row movement. One might make the updates
cascade and expect that to prevent the error:drop table q;
create table q (a bigint references p on update cascade);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
ERROR: update or delete on table "p2" violates foreign key constraint
"q_a_fkey2" on table "q"
DETAIL: Key (a)=(2) is still referenced from table "q".No luck, because again it's the RI delete trigger on p2 that gets
fired. If you make deletes cascade too, an even worse thing happens:drop table q;
create table q (a bigint references p on update cascade on delete cascade);
insert into q values (2);
update p set a = 1;
NOTICE: BEFORE UPDATE on p2
NOTICE: BEFORE DELETE on p2
NOTICE: BEFORE INSERT on p1
NOTICE: AFTER DELETE on p2
NOTICE: AFTER INSERT on p1
UPDATE 1
select * from q;
a
---
(0 rows)The RI delete trigger deleted 2 from q, whereas the expected result
would've been for q to be updated to change 2 to 1.
Thank you for a good summary. That's helpful to catch up on this thread.
This shows that the way we've made these triggers behave in general
can cause some unintended behaviors for foreign keys during
cross-partition updates. I started this thread to do something about
that and sent a patch to prevent cross-partition updates at all when
there are foreign keys pointing to it. As others pointed out, that's
not a great long-term solution to the problem, but that's what we may
have to do in the back-branches if anything at all.
I've started by reviewing the patch for back-patching that the first
patch you posted[1]/messages/by-id/CA+HiwqFvkBCmfwkQX_yBqv2Wz8ugUGiBDxum8=WvVbfU1TXaNg@mail.gmail.com.
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[i];
+
+ if (trigger->tgisinternal &&
+ OidIsValid(trigger->tgconstrrelid) &&
+ trigger->tgfoid == F_RI_FKEY_CASCADE_DEL)
+ {
+ found = true;
+ break;
+ }
+ }
IIUC the above checks if the partition table is referenced by a
foreign key constraint on another table with ON DELETE CASCADE option.
I think we should prevent cross-partition update also when ON DELETE
SET NULL and ON DELETE SET DEFAULT. For example, with the patch, a
tuple in a partition table is still updated to NULL when
cross-partition update as follows:
postgres=# create table p (a numeric primary key) partition by list (a);
CREATE TABLE
postgres=# create table p1 partition of p for values in (1);
CREATE TABLE
postgres=# create table p2 partition of p for values in (2);
CREATE TABLE
postgres=# insert into p values (1);
INSERT 0 1
postgres=# create table q (a int references p(a) on delete set null);
CREATE TABLE
postgres=# insert into q values (1);
INSERT 0 1
postgres=# update p set a = 2;
UPDATE 1
postgres=# table p;
a
---
2
(1 row)
postgres=# table q;
a
---
(1 row)
Regards,
[1]: /messages/by-id/CA+HiwqFvkBCmfwkQX_yBqv2Wz8ugUGiBDxum8=WvVbfU1TXaNg@mail.gmail.com
--
Masahiko Sawada
EDB: https://www.enterprisedb.com/
Thank you for looking at this.
On Mon, Feb 15, 2021 at 4:12 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jan 20, 2021 at 7:04 PM Amit Langote <amitlangote09@gmail.com> wrote:
This shows that the way we've made these triggers behave in general
can cause some unintended behaviors for foreign keys during
cross-partition updates. I started this thread to do something about
that and sent a patch to prevent cross-partition updates at all when
there are foreign keys pointing to it. As others pointed out, that's
not a great long-term solution to the problem, but that's what we may
have to do in the back-branches if anything at all.I've started by reviewing the patch for back-patching that the first
patch you posted[1].+ for (i = 0; i < trigdesc->numtriggers; i++) + { + Trigger *trigger = &trigdesc->triggers[i]; + + if (trigger->tgisinternal && + OidIsValid(trigger->tgconstrrelid) && + trigger->tgfoid == F_RI_FKEY_CASCADE_DEL) + { + found = true; + break; + } + }IIUC the above checks if the partition table is referenced by a
foreign key constraint on another table with ON DELETE CASCADE option.
I think we should prevent cross-partition update also when ON DELETE
SET NULL and ON DELETE SET DEFAULT. For example, with the patch, a
tuple in a partition table is still updated to NULL when
cross-partition update as follows:postgres=# create table p (a numeric primary key) partition by list (a);
CREATE TABLE
postgres=# create table p1 partition of p for values in (1);
CREATE TABLE
postgres=# create table p2 partition of p for values in (2);
CREATE TABLE
postgres=# insert into p values (1);
INSERT 0 1
postgres=# create table q (a int references p(a) on delete set null);
CREATE TABLE
postgres=# insert into q values (1);
INSERT 0 1
postgres=# update p set a = 2;
UPDATE 1
postgres=# table p;
a
---
2
(1 row)postgres=# table q;
a
---(1 row)
Yeah, I agree that's not good.
Actually, I had meant to send an updated version of the patch to apply
in back-branches that would prevent such a cross-partition update, but
never did since starting to work on the patch for HEAD. I have
attached it here.
Regarding the patch, I would have liked if it only prevented the
update when the foreign key that would be violated by the component
delete references the update query's main target relation. If the
foreign key references the source partition directly, then the delete
would be harmless. However there's not enough infrastructure in v12,
v13 branches to determine that, which makes back-patching this a bit
hard. For example, there's no way to tell in the back-branches if the
foreign-key-enforcing triggers of a leaf partition have descended from
the parent table. IOW, I am not so sure anymore if we should try to
do anything in the back-branches.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
prevent-row-movement-on-pk-table.patchapplication/octet-stream; name=prevent-row-movement-on-pk-table.patchDownload
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 2993ba43e3..55d5d8b56a 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1230,7 +1230,38 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
/*
* Row movement, part 1. Delete the tuple, but skip RETURNING processing.
* We want to return rows from INSERT.
+ *
+ * However, if the source partition is being referenced in a foreign key
+ * constraint inherited from some ancestor, then proceeding with the
+ * delete may result violating the constraint.
*/
+ if (resultRelInfo->ri_TrigDesc != NULL &&
+ resultRelInfo->ri_TrigDesc->trig_delete_after_row)
+ {
+ TriggerDesc *trigdesc = resultRelInfo->ri_TrigDesc;
+ int i;
+ bool found = false;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trigger = &trigdesc->triggers[i];
+
+ if (trigger->tgisinternal &&
+ OidIsValid(trigger->tgconstrrelid) &&
+ RI_FKey_trigger_type(trigger->tgfoid) == RI_TRIGGER_PK)
+ {
+ found = true;
+ break;
+ }
+ }
+
+ if (found)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move row being updated to another partition"),
+ errdetail("Moving the row may cause a foreign key involving the source partition to be violated.")));
+ }
+
ExecDelete(mtstate, resultRelInfo, tupleid, oldtuple, planSlot,
epqstate, estate,
false, /* processReturning */
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 07bd5b6434..7f018936b9 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2380,10 +2380,10 @@ DELETE FROM pk WHERE a = 20 RETURNING *;
20
(1 row)
-UPDATE pk SET a = 90 WHERE a = 30 RETURNING *;
+UPDATE pk SET a = 40 WHERE a = 30 RETURNING *;
a
----
- 90
+ 40
(1 row)
SELECT tableoid::regclass, * FROM fk;
@@ -2406,7 +2406,7 @@ INSERT INTO fk VALUES (20), (30);
DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
-UPDATE pk SET a = 90 WHERE a = 30;
+UPDATE pk SET a = 40 WHERE a = 30;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
@@ -2416,6 +2416,11 @@ SELECT tableoid::regclass, * FROM fk;
fk12 | 30
(2 rows)
+-- Limitation: an update of pk causing a row to move between partitions is not
+-- supported in the presence of a foreign key pointing to it
+UPDATE pk SET a = 90 WHERE a = 30;
+ERROR: cannot move row being updated to another partition
+DETAIL: Moving the row may cause a foreign key involving the source partition to be violated.
DROP TABLE fk;
-- test for reported bug: relispartition not set
-- https://postgr.es/m/CA+HiwqHMsRtRYRWYTWavKJ8x14AFsv7bmAV46mYwnfD3vy8goQ@mail.gmail.com
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index c5c9011afc..bc9afe66ae 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1683,7 +1683,7 @@ ALTER TABLE fk ADD FOREIGN KEY (a) REFERENCES pk ON UPDATE SET DEFAULT ON DELETE
CREATE TABLE fk_d PARTITION OF fk DEFAULT;
INSERT INTO fk VALUES (1, 20), (2, 30);
DELETE FROM pk WHERE a = 20 RETURNING *;
-UPDATE pk SET a = 90 WHERE a = 30 RETURNING *;
+UPDATE pk SET a = 40 WHERE a = 30 RETURNING *;
SELECT tableoid::regclass, * FROM fk;
DROP TABLE fk;
@@ -1697,8 +1697,13 @@ ALTER TABLE fk ADD FOREIGN KEY (a) REFERENCES pk ON UPDATE RESTRICT ON DELETE RE
CREATE TABLE fk_d PARTITION OF fk DEFAULT;
INSERT INTO fk VALUES (20), (30);
DELETE FROM pk WHERE a = 20;
-UPDATE pk SET a = 90 WHERE a = 30;
+UPDATE pk SET a = 40 WHERE a = 30;
SELECT tableoid::regclass, * FROM fk;
+
+-- Limitation: an update of pk causing a row to move between partitions is not
+-- supported in the presence of a foreign key pointing to it
+UPDATE pk SET a = 90 WHERE a = 30;
+
DROP TABLE fk;
-- test for reported bug: relispartition not set
Hi Amit,
Here is an updated version of the patch with some cosmetic changes
from the previous version. I moved the code being added to
AfterTriggerSaveEvent() and ExecUpdate() into separate subroutines to
improve readability, hopefully.I tested these patches. It works as expected in case of cross partition
updates, by correctly updating the
referencing table. It works fine for ON UPDATE SET NULL and SET DEFAULT
options as well.
Also, tested by having a table reference only a partition and not the
parent. In this case, the delete
trigger is correctly called when the row is moved out of referenced
partition.
The partition-key-update-1.spec test fails with the following error message
appearing in the diffs.
step s1u3pc: UPDATE foo_range_parted SET a=11 WHERE a=7;
+ERROR: cannot move row being updated to another partition
I think the documentation update is missing from the patches.
Thank you,
Rahila Syed
Hi Rahila,
Thanks for the review.
On Thu, Feb 18, 2021 at 7:08 PM Rahila Syed <rahilasyed90@gmail.com> wrote:
Here is an updated version of the patch with some cosmetic changes
from the previous version. I moved the code being added to
AfterTriggerSaveEvent() and ExecUpdate() into separate subroutines to
improve readability, hopefully.I tested these patches.
It works as expected in case of cross partition updates, by correctly updating the
referencing table. It works fine for ON UPDATE SET NULL and SET DEFAULT options as well.
Also, tested by having a table reference only a partition and not the parent. In this case, the delete
trigger is correctly called when the row is moved out of referenced partition.
I assume these are comments for the v3-0001 & v3-0002 patches...
The partition-key-update-1.spec test fails with the following error message appearing in the diffs.
step s1u3pc: UPDATE foo_range_parted SET a=11 WHERE a=7;
+ERROR: cannot move row being updated to another partition
...whereas, this error happens with the patch I posted in my last
email (prevent-row-movement-on-pk-table.patch) that is not meant to be
considered for HEAD, but for back-branches (if at all). I also see
that cfbot got triggered by it and shows the same failure. I am not
going to try to take care of these failures unless we want to do
something in the back-branches.
To be clear, patches for HEAD do pass make check-world.
I think the documentation update is missing from the patches.
Hmm, I don't think we document the behavior that is improved by the v3
patches as a limitation of any existing feature, neither of foreign
keys referencing partitioned tables nor of the update row movement
feature. So maybe there's nothing in the existing documentation that
is to be updated.
However, the patch does add a new error message for a case that the
patch doesn't handle, so maybe we could document that as a limitation.
Not sure if in the Notes section of the UPDATE reference page which
has some notes on row movement or somewhere else. Do you have
suggestions?
Attaching rebased version of the patches for HEAD to appease the cfbot.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v4-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v4-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From 9ff3f8eef5eb57e8a4fa4022046cd68196262965 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v4 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 354 ++++++++++++++++++++-----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 20 +-
src/bin/psql/describe.c | 6 +-
src/include/commands/trigger.h | 5 +-
src/test/regress/expected/triggers.out | 4 +-
6 files changed, 407 insertions(+), 83 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b2457a6924..4246479f87 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -449,12 +449,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -462,15 +464,34 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
+static void AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -8869,7 +8890,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8882,7 +8904,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -8922,7 +8945,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -8930,6 +8954,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9029,12 +9055,11 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* If the referenced table is a plain relation, create the action triggers
* that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9078,7 +9103,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9127,8 +9153,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9142,14 +9172,16 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9178,6 +9210,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9219,7 +9260,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9298,10 +9342,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9357,6 +9405,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9386,6 +9435,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9403,6 +9460,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9469,6 +9528,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ GetForeignKeyActionTriggers(constrForm, trigrel,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9481,11 +9544,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9508,6 +9575,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9529,6 +9597,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9558,6 +9634,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9586,6 +9664,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9604,7 +9686,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9703,9 +9788,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9726,13 +9815,15 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
@@ -9796,12 +9887,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -9834,13 +9923,135 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
+
+ AttachForeignKeyCheckTriggers(fk, partRelid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
CommandCounterIncrement();
return true;
}
+static void
+GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(constrForm->oid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != constrForm->conrelid)
+ continue;
+ if (trgform->tgrelid != constrForm->confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key constraint %u",
+ constrForm->oid);
+
+ systable_endscan(scan);
+}
+
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+static void
+AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+}
+
+static void
+DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid)
+{
+ Relation trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, partRelid);
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, partRelid);
+
+ table_close(trigrel, RowExclusiveLock);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10641,10 +10852,12 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10684,11 +10897,14 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
@@ -10698,9 +10914,12 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10752,9 +10971,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10809,9 +11031,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
@@ -10822,12 +11047,16 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -16852,20 +17081,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
if (!TRIGGER_FOR_ROW(trigForm->tgtype))
continue;
- /*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
- */
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ /* Don't clone internal triggers. They are dealt with separately. */
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17114,7 +17331,11 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
+
+ DetachForeignKeyCheckTriggers(fk, RelationGetRelid(partRel));
ReleaseSysCache(contup);
}
@@ -17190,6 +17411,13 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys. They will be dealt with separately.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 8908847c6c..f31ffcd9da 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -126,8 +126,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -178,6 +180,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -716,6 +719,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -742,17 +746,16 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -849,7 +852,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1218,6 +1221,82 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ Assert(trigForm->tgparentid == 0);
+ if (trigForm->tgparentid != InvalidOid)
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index eb988d7eb4..e51820b837 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7961,7 +7961,25 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 90000)
+ if (fout->remoteVersion >= 130000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ */
+ appendPQExpBuffer(query,
+ "SELECT tgname, "
+ "tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(oid, false) AS tgdef, "
+ "tgenabled, tableoid, oid "
+ "FROM pg_catalog.pg_trigger t "
+ "WHERE tgrelid = '%u'::pg_catalog.oid "
+ "AND NOT tgisinternal "
+ "AND tgparentid = 0",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 90000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 20af5a92b4..a695f954b8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2973,11 +2973,7 @@ describeOneTableDetails(const char *schemaname,
" 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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
- else if (pset.sversion >= 90000)
+ if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
else if (pset.sversion >= 80300)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..c4f742773c 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -154,7 +154,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
-
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index b263002293..a848d388a9 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3255,7 +3255,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v4-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v4-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From c00679313749bec6ff4eda677eb51f2f05b40f15 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v4 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing of the delete triggers that implement those foreign keys can
result in surprising outcomes for those keys. For example, a given
foreign key's delete trigger which implements the ON DELETE CASCADE
clause of that key will delete any referencing rows, although it
should not, because the referenced row is simply being moved into
another partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
To make this possible, this adjusts AFTER trigger data strucutures
to allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
src/backend/commands/trigger.c | 177 +++++++++++-----
src/backend/executor/execMain.c | 9 +
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 237 ++++++++++++++++++++--
src/backend/utils/adt/ri_triggers.c | 17 +-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 4 +
src/test/regress/expected/foreign_key.out | 146 ++++++++++++-
src/test/regress/sql/foreign_key.sql | 84 ++++++++
9 files changed, 609 insertions(+), 71 deletions(-)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index f31ffcd9da..2d2e17e3a1 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -88,13 +88,18 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static bool SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel);
/*
@@ -2307,7 +2312,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2396,7 +2401,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2521,7 +2526,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2618,7 +2623,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2641,7 +2647,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2762,7 +2768,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -2899,7 +2905,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2933,7 +2940,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else if (fdw_trigtuple != NULL)
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3059,7 +3066,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3347,19 +3354,21 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. For partitioned tables, we never
+ * queue any events for its deferred triggers. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3380,8 +3389,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3575,7 +3584,8 @@ typedef struct AfterTriggersData
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3624,15 +3634,15 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
+ * Get the tuplestore for the current trigger query level, creating it
* if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetCurrentAfterTriggerTuplestore(void)
{
Tuplestorestate *ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
+ ret = afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
if (ret == NULL)
{
MemoryContext oldcxt;
@@ -3651,7 +3661,7 @@ GetCurrentFDWTuplestore(void)
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
+ afterTriggers.query_stack[afterTriggers.query_depth].tuplestore = ret;
}
return ret;
@@ -3985,22 +3995,22 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore = GetCurrentAfterTriggerTuplestore();
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4301,7 +4311,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4685,8 +4696,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5018,7 +5029,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5489,7 +5500,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5503,7 +5515,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5701,7 +5713,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5721,16 +5735,22 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ SkipCrossPartitionUpdateFKeyTrigger(mtstate, trigger, event, rel))
+ continue;
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ if (tuplestore == NULL)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ tuplestore = GetCurrentAfterTriggerTuplestore();
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5804,17 +5824,70 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(tuplestore, newslot);
+ }
+}
+
+/*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+static bool
+SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel)
+{
+ Relation rootRelDesc = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (rootRelDesc->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ return false;
+
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ return true;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ return true;
+ break;
}
+
+ return false;
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index c74ce36ffb..e0edc33d0c 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1424,8 +1424,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 1e285e0349..9ae702c5cb 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 2993ba43e3..70b5a7ec7c 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -42,6 +42,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -387,7 +388,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -714,7 +717,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -752,6 +755,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1110,7 +1118,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1125,7 +1133,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1197,7 +1205,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
@@ -1286,8 +1296,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1300,6 +1311,180 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up trigger events necessary to check that a cross-partition update
+ * of the target partitioned table hasn't broken any foreign keys pointing
+ * to it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ HeapTuple oldtuple;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ Relation sourcePartRelDesc = sourcePartInfo->ri_RelationDesc;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo, mtstate);
+
+ /*
+ * There better not be any foreign keys that point directly to a non-root
+ * ancestor of the target source partition, because we can't enforce them.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted "new" tuple into the root table's slot, after
+ * converting it if needed.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(sourcePartInfo, mtstate);
+ if (newslot != oldslot && map)
+ newslot = execute_attr_map_slot(map->attrMap, newslot, rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(sourcePartRelDesc, tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1453,9 +1638,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1467,14 +1655,38 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1640,7 +1852,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2248,7 +2461,7 @@ ExecModifyTable(PlanState *pstate)
{
case CMD_INSERT:
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 6e3a41062f..555f204fd9 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1259,11 +1259,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index c4f742773c..79b4ba129d 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -205,6 +205,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -224,6 +225,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 943931f65d..d6dd4fd01f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -510,9 +510,13 @@ typedef struct ResultRelInfo
* transition tuple capture or update partition row movement is active.
*/
TupleConversionMap *ri_ChildToRootMap;
+ bool ri_ChildToRootMapValid;
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 07bd5b6434..e2c2c3d756 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2407,7 +2407,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2470,3 +2470,147 @@ DROP SCHEMA fkpart9 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart9.pk
drop cascades to table fkpart9.fk
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart10.pk12 | 2
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 2
+ fkpart10.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 2
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 2
+ fkpart10.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+ tableoid | a
+---------------+---
+ fkpart10.pk11 | 1
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 1
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 4
+ fkpart10.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart10.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart10 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart10.pk
+drop cascades to table fkpart10.fk_parted
+drop cascades to table fkpart10.fk_another
+drop cascades to function fkpart10.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index c5c9011afc..5902b787c5 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1738,3 +1738,87 @@ DELETE FROM fkpart9.pk WHERE a=35;
SELECT * FROM fkpart9.pk;
SELECT * FROM fkpart9.fk;
DROP SCHEMA fkpart9 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+DROP TABLE fkpart10.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart10 CASCADE;
--
2.24.1
On Mon, Feb 15, 2021 at 10:37 PM Amit Langote <amitlangote09@gmail.com> wrote:
Thank you for looking at this.
On Mon, Feb 15, 2021 at 4:12 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Wed, Jan 20, 2021 at 7:04 PM Amit Langote <amitlangote09@gmail.com> wrote:
This shows that the way we've made these triggers behave in general
can cause some unintended behaviors for foreign keys during
cross-partition updates. I started this thread to do something about
that and sent a patch to prevent cross-partition updates at all when
there are foreign keys pointing to it. As others pointed out, that's
not a great long-term solution to the problem, but that's what we may
have to do in the back-branches if anything at all.I've started by reviewing the patch for back-patching that the first
patch you posted[1].+ for (i = 0; i < trigdesc->numtriggers; i++) + { + Trigger *trigger = &trigdesc->triggers[i]; + + if (trigger->tgisinternal && + OidIsValid(trigger->tgconstrrelid) && + trigger->tgfoid == F_RI_FKEY_CASCADE_DEL) + { + found = true; + break; + } + }IIUC the above checks if the partition table is referenced by a
foreign key constraint on another table with ON DELETE CASCADE option.
I think we should prevent cross-partition update also when ON DELETE
SET NULL and ON DELETE SET DEFAULT. For example, with the patch, a
tuple in a partition table is still updated to NULL when
cross-partition update as follows:postgres=# create table p (a numeric primary key) partition by list (a);
CREATE TABLE
postgres=# create table p1 partition of p for values in (1);
CREATE TABLE
postgres=# create table p2 partition of p for values in (2);
CREATE TABLE
postgres=# insert into p values (1);
INSERT 0 1
postgres=# create table q (a int references p(a) on delete set null);
CREATE TABLE
postgres=# insert into q values (1);
INSERT 0 1
postgres=# update p set a = 2;
UPDATE 1
postgres=# table p;
a
---
2
(1 row)postgres=# table q;
a
---(1 row)
Yeah, I agree that's not good.
Actually, I had meant to send an updated version of the patch to apply
in back-branches that would prevent such a cross-partition update, but
never did since starting to work on the patch for HEAD. I have
attached it here.
Thank you for updating the patch!
Regarding the patch, I would have liked if it only prevented the
update when the foreign key that would be violated by the component
delete references the update query's main target relation. If the
foreign key references the source partition directly, then the delete
would be harmless. However there's not enough infrastructure in v12,
v13 branches to determine that, which makes back-patching this a bit
hard. For example, there's no way to tell in the back-branches if the
foreign-key-enforcing triggers of a leaf partition have descended from
the parent table. IOW, I am not so sure anymore if we should try to
do anything in the back-branches.
Hmm I'm not sure the necessity of figuring out foreign-key-enforcing
triggers of a leaf partition have descended from the parent table. I
might be missing something but even if the foreign key references the
leaf partition directly, the same problem could happen if doing a
cross-partition update, is that right?
Also, the updated patch prevents a cross-partition update even when
the foreign key references another column of itself. This kind of
cross-update works as expected for now so it seems not to need to
disallow that. What do you think? The scenario is:
create table p (a int primary key, b int references p(a) on delete
cascade) partition by list (a);
create table p1 partition of p for values in (1);
create table p2 partition of p for values in (2);
insert into p values (1, 1);
update p set a = 2, b = 2 where a = 1;
ERROR: cannot move row being updated to another partition
DETAIL: Moving the row may cause a foreign key involving the source
partition to be violated.
Regards,
--
Masahiko Sawada
EDB: https://www.enterprisedb.com/
On Fri, Feb 19, 2021 at 5:04 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Mon, Feb 15, 2021 at 10:37 PM Amit Langote <amitlangote09@gmail.com> wrote:
Regarding the patch, I would have liked if it only prevented the
update when the foreign key that would be violated by the component
delete references the update query's main target relation. If the
foreign key references the source partition directly, then the delete
would be harmless. However there's not enough infrastructure in v12,
v13 branches to determine that, which makes back-patching this a bit
hard. For example, there's no way to tell in the back-branches if the
foreign-key-enforcing triggers of a leaf partition have descended from
the parent table. IOW, I am not so sure anymore if we should try to
do anything in the back-branches.Hmm I'm not sure the necessity of figuring out foreign-key-enforcing
triggers of a leaf partition have descended from the parent table. I
might be missing something but even if the foreign key references the
leaf partition directly, the same problem could happen if doing a
cross-partition update, is that right?
Actually not, because in that case the referencing relations only care
about the contents of that leaf partition, so it's okay that the ON
DELETE actions are performed in that case, or IOW, no need to abort
the update. Contrast that with when the foreign key references the
parent table being updated, where both the old and the new row
logically belong to the table being referenced, so performing ON
DELETE actions using the source leaf partition's trigger is wrong.
Does that make sense?
Also, the updated patch prevents a cross-partition update even when
the foreign key references another column of itself. This kind of
cross-update works as expected for now so it seems not to need to
disallow that. What do you think? The scenario is:create table p (a int primary key, b int references p(a) on delete
cascade) partition by list (a);
create table p1 partition of p for values in (1);
create table p2 partition of p for values in (2);
insert into p values (1, 1);
update p set a = 2, b = 2 where a = 1;
ERROR: cannot move row being updated to another partition
DETAIL: Moving the row may cause a foreign key involving the source
partition to be violated.
Hmm yes, it would be nice to not cause an error in this case. But we
don't have enough details about the actual foreign key in this part of
the code (ExecUpdate). Given the current ResultRelInfo
infrastructure, this code can only look at the trigger objects to get
some details about the foreign key, such as whether the relation being
operated on is the FK relation or the PK relation. More detailed
properties of those foreign keys are only checked inside the trigger's
functions (ri_trigger.c) and perhaps also in the dispatch code in
trigger.c: AfterTriggerSaveEvent(). However, it would be hard to
detect in those modules that a delete trigger event indeed comes from
a delete performed as part of a cross-partition update, that is,
without significantly changing their interfaces.
Anyway, I am no longer sure if we should do anything in the back
branches, which the patch you have been looking at is for. There have
not been many field complaints about this other than the one that
started this thread. Also, I suspect that aborting the
cross-partition updates for any partitioned table that is referenced
in a foreign key will annoy users, especially those who simply don't
use ON DELETE/UPDATE clauses.
So, it may be better to focus on the patches for master that, instead
of giving an error, make the cross-partition updates just work.
--
Amit Langote
EDB: http://www.enterprisedb.com
Hi Amit,
Sorry for the late reply.
I assume these are comments for the v3-0001 & v3-0002 patches...
Yes, those were comments for patches on master.
The partition-key-update-1.spec test fails with the following error
message appearing in the diffs.
step s1u3pc: UPDATE foo_range_parted SET a=11 WHERE a=7;
+ERROR: cannot move row being updated to another partition...whereas, this error happens with the patch I posted in my last
email (prevent-row-movement-on-pk-table.patch) that is not meant to be
considered for HEAD, but for back-branches (if at all). I also see
that cfbot got triggered by it and shows the same failure. I am not
going to try to take care of these failures unless we want to do
something in the back-branches.To be clear, patches for HEAD do pass make check-world.
OK.
I think the documentation update is missing from the patches.
Hmm, I don't think we document the behavior that is improved by the v3
patches as a limitation of any existing feature, neither of foreign
keys referencing partitioned tables nor of the update row movement
feature. So maybe there's nothing in the existing documentation that
is to be updated.However, the patch does add a new error message for a case that the
patch doesn't handle, so maybe we could document that as a limitation.
Not sure if in the Notes section of the UPDATE reference page which
has some notes on row movement or somewhere else. Do you have
suggestions?You are right, I could not find any direct explanation of the impact of
row movement during
UPDATE on a referencing table in the PostgreSQL docs.
The two documents that come close are either:
1. https://www.postgresql.org/docs/13/trigger-definition.html .
The para starting with "If an UPDATE on a partitioned table causes a row to
move to another partition"
However, this does not describe the behaviour of internal triggers which
is the focus of this patch.
2. Another one like you mentioned,
https://www.postgresql.org/docs/11/sql-update.html
This has explanation for row movement behaviour for partitioned table but
does not explain
any impact of such behaviour on a referencing table.
I think it is worth adding some explanation in this document. Thus,
explaining
impact on referencing tables here, as it already describes behaviour of
UPDATE on a partitioned table.
Thank you.
Rahila Syed
On Mon, Feb 22, 2021 at 3:04 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Fri, Feb 19, 2021 at 5:04 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Mon, Feb 15, 2021 at 10:37 PM Amit Langote <amitlangote09@gmail.com> wrote:
Regarding the patch, I would have liked if it only prevented the
update when the foreign key that would be violated by the component
delete references the update query's main target relation. If the
foreign key references the source partition directly, then the delete
would be harmless. However there's not enough infrastructure in v12,
v13 branches to determine that, which makes back-patching this a bit
hard. For example, there's no way to tell in the back-branches if the
foreign-key-enforcing triggers of a leaf partition have descended from
the parent table. IOW, I am not so sure anymore if we should try to
do anything in the back-branches.Hmm I'm not sure the necessity of figuring out foreign-key-enforcing
triggers of a leaf partition have descended from the parent table. I
might be missing something but even if the foreign key references the
leaf partition directly, the same problem could happen if doing a
cross-partition update, is that right?Actually not, because in that case the referencing relations only care
about the contents of that leaf partition, so it's okay that the ON
DELETE actions are performed in that case, or IOW, no need to abort
the update. Contrast that with when the foreign key references the
parent table being updated, where both the old and the new row
logically belong to the table being referenced, so performing ON
DELETE actions using the source leaf partition's trigger is wrong.Does that make sense?
That makes sense. Thanks for your explanation.
One more thing, users are aware of a cross-partition update is
internally converted to DELETE + INSERT (i.g., documented somewhere)?
If not, users might get confused if a tuple referencing a partition
table through a foreign key constraint with ON DELETE CASCADE is
deleted when UPDATE on the parent table.
Also, the updated patch prevents a cross-partition update even when
the foreign key references another column of itself. This kind of
cross-update works as expected for now so it seems not to need to
disallow that. What do you think? The scenario is:create table p (a int primary key, b int references p(a) on delete
cascade) partition by list (a);
create table p1 partition of p for values in (1);
create table p2 partition of p for values in (2);
insert into p values (1, 1);
update p set a = 2, b = 2 where a = 1;
ERROR: cannot move row being updated to another partition
DETAIL: Moving the row may cause a foreign key involving the source
partition to be violated.Hmm yes, it would be nice to not cause an error in this case. But we
don't have enough details about the actual foreign key in this part of
the code (ExecUpdate). Given the current ResultRelInfo
infrastructure, this code can only look at the trigger objects to get
some details about the foreign key, such as whether the relation being
operated on is the FK relation or the PK relation. More detailed
properties of those foreign keys are only checked inside the trigger's
functions (ri_trigger.c) and perhaps also in the dispatch code in
trigger.c: AfterTriggerSaveEvent(). However, it would be hard to
detect in those modules that a delete trigger event indeed comes from
a delete performed as part of a cross-partition update, that is,
without significantly changing their interfaces.
Agreed.
Anyway, I am no longer sure if we should do anything in the back
branches, which the patch you have been looking at is for. There have
not been many field complaints about this other than the one that
started this thread. Also, I suspect that aborting the
cross-partition updates for any partitioned table that is referenced
in a foreign key will annoy users, especially those who simply don't
use ON DELETE/UPDATE clauses.
I thought to disallow creating foreign key constraint referencing a
partitioned table with ON DELETE/UPDATE actions other than NO ACTION
and RESTRICT but it also seems to annoy users.
So, it may be better to focus on the patches for master that, instead
of giving an error, make the cross-partition updates just work.
Okay, let's focus on the patches for master. I'll look at that patch next week.
Regards,
--
Masahiko Sawada
EDB: https://www.enterprisedb.com/
Hi Rahila,
On Wed, Feb 24, 2021 at 3:07 PM Rahila Syed <rahilasyed90@gmail.com> wrote:
I think the documentation update is missing from the patches.
Hmm, I don't think we document the behavior that is improved by the v3
patches as a limitation of any existing feature, neither of foreign
keys referencing partitioned tables nor of the update row movement
feature. So maybe there's nothing in the existing documentation that
is to be updated.However, the patch does add a new error message for a case that the
patch doesn't handle, so maybe we could document that as a limitation.
Not sure if in the Notes section of the UPDATE reference page which
has some notes on row movement or somewhere else. Do you have
suggestions?You are right, I could not find any direct explanation of the impact of row movement during
UPDATE on a referencing table in the PostgreSQL docs.The two documents that come close are either:
Thanks for looking those up.
1. https://www.postgresql.org/docs/13/trigger-definition.html .
The para starting with "If an UPDATE on a partitioned table causes a row to move to another partition"
However, this does not describe the behaviour of internal triggers which is the focus of this patch.
The paragraph does talk about a very related topic, but, like you, I
am not very excited about adding a line here about what we're doing
with internal triggers.
2. Another one like you mentioned, https://www.postgresql.org/docs/11/sql-update.html
This has explanation for row movement behaviour for partitioned table but does not explain
any impact of such behaviour on a referencing table.
I think it is worth adding some explanation in this document. Thus, explaining
impact on referencing tables here, as it already describes behaviour of
UPDATE on a partitioned table.
ISTM the description of the case that will now be prevented seems too
obscure to make into a documentation line, but I tried. Please check.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v5-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v5-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From 4a08bb5b0216d6d1ca20b2e0cbd0da8a4e211fa9 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v5 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 354 ++++++++++++++++++++-----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 20 +-
src/bin/psql/describe.c | 6 +-
src/include/commands/trigger.h | 5 +-
src/test/regress/expected/triggers.out | 4 +-
6 files changed, 407 insertions(+), 83 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b2457a6924..4246479f87 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -449,12 +449,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -462,15 +464,34 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
+static void AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel);
+static void DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -8869,7 +8890,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8882,7 +8904,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -8922,7 +8945,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -8930,6 +8954,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9029,12 +9055,11 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* If the referenced table is a plain relation, create the action triggers
* that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9078,7 +9103,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9127,8 +9153,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9142,14 +9172,16 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9178,6 +9210,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9219,7 +9260,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9298,10 +9342,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9357,6 +9405,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9386,6 +9435,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9403,6 +9460,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9469,6 +9528,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ GetForeignKeyActionTriggers(constrForm, trigrel,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9481,11 +9544,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9508,6 +9575,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9529,6 +9597,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9558,6 +9634,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9586,6 +9664,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9604,7 +9686,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9703,9 +9788,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9726,13 +9815,15 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
@@ -9796,12 +9887,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -9834,13 +9923,135 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
+
+ AttachForeignKeyCheckTriggers(fk, partRelid, parentInsTrigger,
+ parentUpdTrigger, trigrel);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
CommandCounterIncrement();
return true;
}
+static void
+GetForeignKeyActionTriggers(Form_pg_constraint constrForm,
+ Relation trigrel,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(constrForm->oid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != constrForm->conrelid)
+ continue;
+ if (trgform->tgrelid != constrForm->confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key constraint %u",
+ constrForm->oid);
+
+ systable_endscan(scan);
+}
+
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+static void
+AttachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Relation trigrel)
+{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+}
+
+static void
+DetachForeignKeyCheckTriggers(ForeignKeyCacheInfo *fk, Oid partRelid)
+{
+ Relation trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid, partRelid);
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid, partRelid);
+
+ table_close(trigrel, RowExclusiveLock);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10641,10 +10852,12 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10684,11 +10897,14 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
@@ -10698,9 +10914,12 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10752,9 +10971,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10809,9 +11031,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
@@ -10822,12 +11047,16 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -16852,20 +17081,8 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
if (!TRIGGER_FOR_ROW(trigForm->tgtype))
continue;
- /*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
- */
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ /* Don't clone internal triggers. They are dealt with separately. */
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17114,7 +17331,11 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
+
+ DetachForeignKeyCheckTriggers(fk, RelationGetRelid(partRel));
ReleaseSysCache(contup);
}
@@ -17190,6 +17411,13 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys. They will be dealt with separately.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 8908847c6c..f31ffcd9da 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -126,8 +126,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -178,6 +180,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -716,6 +719,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -742,17 +746,16 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -849,7 +852,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1218,6 +1221,82 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ Assert(trigForm->tgparentid == 0);
+ if (trigForm->tgparentid != InvalidOid)
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index eb988d7eb4..e51820b837 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7961,7 +7961,25 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 90000)
+ if (fout->remoteVersion >= 130000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ */
+ appendPQExpBuffer(query,
+ "SELECT tgname, "
+ "tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(oid, false) AS tgdef, "
+ "tgenabled, tableoid, oid "
+ "FROM pg_catalog.pg_trigger t "
+ "WHERE tgrelid = '%u'::pg_catalog.oid "
+ "AND NOT tgisinternal "
+ "AND tgparentid = 0",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 90000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 20af5a92b4..a695f954b8 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2973,11 +2973,7 @@ describeOneTableDetails(const char *schemaname,
" 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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
- else if (pset.sversion >= 90000)
+ if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
else if (pset.sversion >= 80300)
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..c4f742773c 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -154,7 +154,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
-
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index b263002293..a848d388a9 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3255,7 +3255,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v5-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v5-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From dec444d50169d729490994264aae85bb66e5b05b Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v5 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing of the delete triggers that implement those foreign keys can
result in surprising outcomes for those keys. For example, a given
foreign key's delete trigger which implements the ON DELETE CASCADE
clause of that key will delete any referencing rows, although it
should not, because the referenced row is simply being moved into
another partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
To make this possible, this adjusts AFTER trigger data strucutures
to allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 177 +++++++++++-----
src/backend/executor/execMain.c | 9 +
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 237 ++++++++++++++++++++--
src/backend/utils/adt/ri_triggers.c | 17 +-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 4 +
src/test/regress/expected/foreign_key.out | 146 ++++++++++++-
src/test/regress/sql/foreign_key.sql | 84 ++++++++
10 files changed, 616 insertions(+), 71 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index f31ffcd9da..2d2e17e3a1 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -88,13 +88,18 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static bool SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel);
/*
@@ -2307,7 +2312,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2396,7 +2401,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2521,7 +2526,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2618,7 +2623,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2641,7 +2647,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2762,7 +2768,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -2899,7 +2905,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2933,7 +2940,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else if (fdw_trigtuple != NULL)
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3059,7 +3066,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3347,19 +3354,21 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. For partitioned tables, we never
+ * queue any events for its deferred triggers. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3380,8 +3389,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3575,7 +3584,8 @@ typedef struct AfterTriggersData
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3624,15 +3634,15 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
+ * Get the tuplestore for the current trigger query level, creating it
* if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetCurrentAfterTriggerTuplestore(void)
{
Tuplestorestate *ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
+ ret = afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
if (ret == NULL)
{
MemoryContext oldcxt;
@@ -3651,7 +3661,7 @@ GetCurrentFDWTuplestore(void)
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
+ afterTriggers.query_stack[afterTriggers.query_depth].tuplestore = ret;
}
return ret;
@@ -3985,22 +3995,22 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore = GetCurrentAfterTriggerTuplestore();
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4301,7 +4311,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4685,8 +4696,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5018,7 +5029,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5489,7 +5500,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5503,7 +5515,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5701,7 +5713,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5721,16 +5735,22 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ SkipCrossPartitionUpdateFKeyTrigger(mtstate, trigger, event, rel))
+ continue;
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ if (tuplestore == NULL)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ tuplestore = GetCurrentAfterTriggerTuplestore();
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5804,17 +5824,70 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(tuplestore, newslot);
+ }
+}
+
+/*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+static bool
+SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel)
+{
+ Relation rootRelDesc = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (rootRelDesc->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ return false;
+
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ return true;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ return true;
+ break;
}
+
+ return false;
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index c74ce36ffb..e0edc33d0c 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1424,8 +1424,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 1e285e0349..9ae702c5cb 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 2993ba43e3..70b5a7ec7c 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -42,6 +42,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -387,7 +388,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -714,7 +717,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -752,6 +755,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1110,7 +1118,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1125,7 +1133,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1197,7 +1205,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
@@ -1286,8 +1296,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1300,6 +1311,180 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up trigger events necessary to check that a cross-partition update
+ * of the target partitioned table hasn't broken any foreign keys pointing
+ * to it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ HeapTuple oldtuple;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ Relation sourcePartRelDesc = sourcePartInfo->ri_RelationDesc;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo, mtstate);
+
+ /*
+ * There better not be any foreign keys that point directly to a non-root
+ * ancestor of the target source partition, because we can't enforce them.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted "new" tuple into the root table's slot, after
+ * converting it if needed.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(sourcePartInfo, mtstate);
+ if (newslot != oldslot && map)
+ newslot = execute_attr_map_slot(map->attrMap, newslot, rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(sourcePartRelDesc, tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1453,9 +1638,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1467,14 +1655,38 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1640,7 +1852,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2248,7 +2461,7 @@ ExecModifyTable(PlanState *pstate)
{
case CMD_INSERT:
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 6e3a41062f..555f204fd9 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1259,11 +1259,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index c4f742773c..79b4ba129d 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -205,6 +205,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -224,6 +225,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 943931f65d..d6dd4fd01f 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -510,9 +510,13 @@ typedef struct ResultRelInfo
* transition tuple capture or update partition row movement is active.
*/
TupleConversionMap *ri_ChildToRootMap;
+ bool ri_ChildToRootMapValid;
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 07bd5b6434..e2c2c3d756 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2407,7 +2407,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2470,3 +2470,147 @@ DROP SCHEMA fkpart9 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart9.pk
drop cascades to table fkpart9.fk
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart10.pk12 | 2
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 2
+ fkpart10.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 2
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 2
+ fkpart10.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+ tableoid | a
+---------------+---
+ fkpart10.pk11 | 1
+ fkpart10.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+-------------+---
+ fkpart10.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart10.fk1 | 1
+ fkpart10.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart10.fk_another | 4
+ fkpart10.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart10.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart10 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart10.pk
+drop cascades to table fkpart10.fk_parted
+drop cascades to table fkpart10.fk_another
+drop cascades to function fkpart10.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index c5c9011afc..5902b787c5 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1738,3 +1738,87 @@ DELETE FROM fkpart9.pk WHERE a=35;
SELECT * FROM fkpart9.pk;
SELECT * FROM fkpart9.fk;
DROP SCHEMA fkpart9 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart10
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart10.pk VALUES (1), (3);
+INSERT INTO fkpart10.fk VALUES (1), (3);
+INSERT INTO fkpart10.fk_parted VALUES (1), (3);
+INSERT INTO fkpart10.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart10.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart10.fk WHERE a = 4;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart10.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart10.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart10.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart10.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart10.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart10.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart10.fk;
+DROP TABLE fkpart10.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart10.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart10.pk FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart10.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart10.print_row();
+UPDATE fkpart10.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart10.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart10 CASCADE;
--
2.24.1
On Fri, Feb 26, 2021 at 4:30 PM Amit Langote <amitlangote09@gmail.com> wrote:
Hi Rahila,
On Wed, Feb 24, 2021 at 3:07 PM Rahila Syed <rahilasyed90@gmail.com> wrote:
I think the documentation update is missing from the patches.
Hmm, I don't think we document the behavior that is improved by the v3
patches as a limitation of any existing feature, neither of foreign
keys referencing partitioned tables nor of the update row movement
feature. So maybe there's nothing in the existing documentation that
is to be updated.However, the patch does add a new error message for a case that the
patch doesn't handle, so maybe we could document that as a limitation.
Not sure if in the Notes section of the UPDATE reference page which
has some notes on row movement or somewhere else. Do you have
suggestions?You are right, I could not find any direct explanation of the impact of row movement during
UPDATE on a referencing table in the PostgreSQL docs.The two documents that come close are either:
Thanks for looking those up.
1. https://www.postgresql.org/docs/13/trigger-definition.html .
The para starting with "If an UPDATE on a partitioned table causes a row to move to another partition"
However, this does not describe the behaviour of internal triggers which is the focus of this patch.The paragraph does talk about a very related topic, but, like you, I
am not very excited about adding a line here about what we're doing
with internal triggers.2. Another one like you mentioned, https://www.postgresql.org/docs/11/sql-update.html
This has explanation for row movement behaviour for partitioned table but does not explain
any impact of such behaviour on a referencing table.
I think it is worth adding some explanation in this document. Thus, explaining
impact on referencing tables here, as it already describes behaviour of
UPDATE on a partitioned table.ISTM the description of the case that will now be prevented seems too
obscure to make into a documentation line, but I tried. Please check.
I looked at the 0001 patch and here are random comments. Please ignore
a comment if it is already discussed.
---
@@ -9077,7 +9102,8 @@ addFkRecurseReferenced(List **wqueue, Constraint
*fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9126,8 +9152,12 @@ addFkRecurseReferencing(List **wqueue,
Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid
*ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
We need to update the function comments as well.
---
I think it's better to add comments for newly added functions such as
GetForeignKeyActionTriggers() and GetForeignKeyCheckTriggers() etc.
Those functions have no comment at all.
BTW, those two functions out of newly added four functions:
AttachForeignKeyCheckTriggers() and DetachForeignKeyCheckTriggers(),
have only one user. Can we past the functions body at where each
function is called?
---
/*
* If the referenced table is a plain relation, create the action triggers
* that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
The comment needs to be updated.
---
/*
* If the referencing relation is a plain table, add the check triggers to
* it and, if necessary, schedule it to be checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
Same as above.
---
I think TriggerSetParentTrigger needs to free the heap tuple copied by
heap_copytuple().
---
+ Assert(trigForm->tgparentid == 0);
+ if (trigForm->tgparentid != InvalidOid)
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
I think the first condition in Assert() can be
!OidIsValid(trigForm->tgparentid) and the second condition in 'if'
statement can be OidIsValid(trigForm->tgparentid). So those are
redundant?
---
- if (fout->remoteVersion >= 90000)
+ if (fout->remoteVersion >= 130000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ */
+ appendPQExpBuffer(query,
+ "SELECT tgname, "
+ "tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(oid,
false) AS tgdef, "
+ "tgenabled, tableoid, oid "
+ "FROM pg_catalog.pg_trigger t "
+ "WHERE tgrelid = '%u'::pg_catalog.oid "
+ "AND NOT tgisinternal "
+ "AND tgparentid = 0",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 90000)
You meant 140000 instead of 130000?
Or is this change really needed? This change added one condition
"tgparentid = 0" but IIUC I think triggers that are NOT tgisinternal
are always tgparentid = 0. Also, it seems it is true both before and
after this patch.
---
@@ -2973,11 +2973,7 @@ describeOneTableDetails(const char *schemaname,
" 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"
- " AND refclassid =
'pg_catalog.pg_trigger'::pg_catalog.regclass))");
- else if (pset.sversion >= 90000)
+ if (pset.sversion >= 90000)
I think we cannot remove this code. For instance, in PG13 since the
trisinternal of a user-defined trigger that has descended from its
parent table is true, executing \d against PG13 by the patched psql
won't show that trigger.
I'll look at 0002 patch.
Regards,
--
Masahiko Sawada
EDB: https://www.enterprisedb.com/
Sawada-san,
On Wed, Mar 10, 2021 at 4:51 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
I looked at the 0001 patch and here are random comments. Please ignore
a comment if it is already discussed.
Thanks a lot for the review and sorry for the delay in replying.
--- @@ -9077,7 +9102,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, partIndexId, constrOid, numfks, mapped_pkattnum, fkattnum, pfeqoperators, ppeqoperators, ffeqoperators, - old_check_ok); + old_check_ok, + deleteTriggerOid, updateTriggerOid);/* Done -- clean up (but keep the lock) */ table_close(partRel, NoLock); @@ -9126,8 +9152,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - bool old_check_ok, LOCKMODE lockmode) + bool old_check_ok, LOCKMODE lockmode, + Oid parentInsTrigger, Oid parentUpdTrigger) {We need to update the function comments as well.
Done.
---
I think it's better to add comments for newly added functions such as
GetForeignKeyActionTriggers() and GetForeignKeyCheckTriggers() etc.
Those functions have no comment at all.
I've added comments.
BTW, those two functions out of newly added four functions:
AttachForeignKeyCheckTriggers() and DetachForeignKeyCheckTriggers(),
have only one user. Can we past the functions body at where each
function is called?
I made those pieces of code into functions because I thought future
patches may have a need for them. But maybe those future patches
should do the refactoring, so I've incorporated their code into the
respective callers as you suggest.
--- /* * If the referenced table is a plain relation, create the action triggers * that enforce the constraint. */ - if (pkrel->rd_rel->relkind == RELKIND_RELATION) - { - createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel), - fkconstraint, - constrOid, indexOid); - } + createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel), + fkconstraint, + constrOid, indexOid, + parentDelTrigger, parentUpdTrigger, + &deleteTriggerOid, &updateTriggerOid);The comment needs to be updated. --- /* * If the referencing relation is a plain table, add the check triggers to * it and, if necessary, schedule it to be checked in Phase 3. * * If the relation is partitioned, drill down to do it to its partitions. */ + createForeignKeyCheckTriggers(RelationGetRelid(rel), + RelationGetRelid(pkrel), + fkconstraint, + parentConstr, + indexOid, + parentInsTrigger, parentUpdTrigger, + &insertTriggerOid, &updateTriggerOid);Same as above.
Done and done.
---
I think TriggerSetParentTrigger needs to free the heap tuple copied by
heap_copytuple().
Oops, done.
--- + Assert(trigForm->tgparentid == 0); + if (trigForm->tgparentid != InvalidOid) + elog(ERROR, "trigger %u already has a parent trigger", + childTrigId);I think the first condition in Assert() can be
!OidIsValid(trigForm->tgparentid) and the second condition in 'if'
statement can be OidIsValid(trigForm->tgparentid). So those are
redundant?
Ah, indeed. I've kept the if () elog(...) after applying your suggested change.
--- - if (fout->remoteVersion >= 90000) + if (fout->remoteVersion >= 130000) + { + /* + * NB: think not to use pretty=true in pg_get_triggerdef. It + * could result in non-forward-compatible dumps of WHEN clauses + * due to under-parenthesization. + */ + appendPQExpBuffer(query, + "SELECT tgname, " + "tgfoid::pg_catalog.regproc AS tgfname, " + "pg_catalog.pg_get_triggerdef(oid, false) AS tgdef, " + "tgenabled, tableoid, oid " + "FROM pg_catalog.pg_trigger t " + "WHERE tgrelid = '%u'::pg_catalog.oid " + "AND NOT tgisinternal " + "AND tgparentid = 0", + tbinfo->dobj.catId.oid); + } + else if (fout->remoteVersion >= 90000)You meant 140000 instead of 130000?
I think I used 130000 because tgparentid was added in v13, but maybe
140000 is right, because for v13 the existing condition (NOT
tgisnternal) already gets us the intended triggers.
Or is this change really needed? This change added one condition
"tgparentid = 0" but IIUC I think triggers that are NOT tgisinternal
are always tgparentid = 0. Also, it seems it is true both before and
after this patch.
Actually, as noted in the commit message, I'm intending to change
tgisnternal to only be true for triggers generated by foreign keys and
no longer for partitions' user-defined triggers that are inherited.
So whereas NOT tgisnternal would suffice to exclude partitions'
inherited triggers before, that would no longer be the case with this
patch; AND tgparentid = 0 will be needed for that.
--- @@ -2973,11 +2973,7 @@ describeOneTableDetails(const char *schemaname, " 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" - " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))"); - else if (pset.sversion >= 90000) + if (pset.sversion >= 90000)I think we cannot remove this code. For instance, in PG13 since the
trisinternal of a user-defined trigger that has descended from its
parent table is true, executing \d against PG13 by the patched psql
won't show that trigger.
I think you're right. I simply needed to change the if condition as follows:
- if (pset.sversion >= 110000)
+ if (pset.sversion >= 110000 && pset.sversion < 140000)
Note that without this change, this code ends up revealing partitions'
foreign key triggers, because we will now be marking them dependent on
their parent trigger, which wasn't the case before this patch.
I'll look at 0002 patch.
Actually, I found a big hole in my assumptions around deferrable
foreign constraints, invalidating the approach I took in 0002 to use a
query-lifetime tuplestore to record root parent tuples. I'm trying to
find a way to make the tuplestore transaction-lifetime so that the
patch still works.
In the meantime, I'm attaching an updated set with 0001 changed per
your comments.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v6-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v6-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From e63978170de89fda77cbd08147ff8cb620e1284d Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v6 2/3] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing of the delete triggers that implement those foreign keys can
result in surprising outcomes for those keys. For example, a given
foreign key's delete trigger which implements the ON DELETE CASCADE
clause of that key will delete any referencing rows, although it
should not, because the referenced row is simply being moved into
another partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
To make this possible, this adjusts AFTER trigger data strucutures
to allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 177 +++++++++++-----
src/backend/executor/execMain.c | 9 +
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 237 ++++++++++++++++++++--
src/backend/utils/adt/ri_triggers.c | 17 +-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 4 +
src/test/regress/expected/foreign_key.out | 150 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 85 +++++++-
10 files changed, 616 insertions(+), 76 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 1298a53e86..27a2bf1d7c 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -88,13 +88,18 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static bool SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel);
/*
@@ -2307,7 +2312,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2396,7 +2401,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2521,7 +2526,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2618,7 +2623,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2641,7 +2647,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2762,7 +2768,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -2899,7 +2905,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2933,7 +2940,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else if (fdw_trigtuple != NULL)
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3059,7 +3066,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3347,19 +3354,21 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. For partitioned tables, we never
+ * queue any events for its deferred triggers. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3380,8 +3389,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3575,7 +3584,8 @@ typedef struct AfterTriggersData
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3626,15 +3636,15 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
+ * Get the tuplestore for the current trigger query level, creating it
* if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetCurrentAfterTriggerTuplestore(void)
{
Tuplestorestate *ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
+ ret = afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
if (ret == NULL)
{
MemoryContext oldcxt;
@@ -3653,7 +3663,7 @@ GetCurrentFDWTuplestore(void)
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
+ afterTriggers.query_stack[afterTriggers.query_depth].tuplestore = ret;
}
return ret;
@@ -3987,22 +3997,22 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore = GetCurrentAfterTriggerTuplestore();
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4303,7 +4313,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4712,8 +4723,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5047,7 +5058,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5518,7 +5529,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5532,7 +5544,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5715,7 +5727,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5735,16 +5749,22 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ SkipCrossPartitionUpdateFKeyTrigger(mtstate, trigger, event, rel))
+ continue;
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ if (tuplestore == NULL)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ tuplestore = GetCurrentAfterTriggerTuplestore();
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5818,17 +5838,70 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(tuplestore, newslot);
+ }
+}
+
+/*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+static bool
+SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel)
+{
+ Relation rootRelDesc = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (rootRelDesc->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ return false;
+
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ return true;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ return true;
+ break;
}
+
+ return false;
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 0648dd82ba..af72209515 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1424,8 +1424,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 1e285e0349..9ae702c5cb 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 2993ba43e3..70b5a7ec7c 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -42,6 +42,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -387,7 +388,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -714,7 +717,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -752,6 +755,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1110,7 +1118,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1125,7 +1133,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1197,7 +1205,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
@@ -1286,8 +1296,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1300,6 +1311,180 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up trigger events necessary to check that a cross-partition update
+ * of the target partitioned table hasn't broken any foreign keys pointing
+ * to it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ HeapTuple oldtuple;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ Relation sourcePartRelDesc = sourcePartInfo->ri_RelationDesc;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo, mtstate);
+
+ /*
+ * There better not be any foreign keys that point directly to a non-root
+ * ancestor of the target source partition, because we can't enforce them.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted "new" tuple into the root table's slot, after
+ * converting it if needed.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(sourcePartInfo, mtstate);
+ if (newslot != oldslot && map)
+ newslot = execute_attr_map_slot(map->attrMap, newslot, rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(sourcePartRelDesc, tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1453,9 +1638,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1467,14 +1655,38 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1640,7 +1852,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2248,7 +2461,7 @@ ExecModifyTable(PlanState *pstate)
{
case CMD_INSERT:
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 09a2ad2881..f2ed644ade 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1263,11 +1263,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index a46d2734c9..e92c349ad3 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -206,6 +206,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -225,6 +226,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index e31ad6204e..34eaca87f2 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -510,9 +510,13 @@ typedef struct ResultRelInfo
* transition tuple capture or update partition row movement is active.
*/
TupleConversionMap *ri_ChildToRootMap;
+ bool ri_ChildToRootMapValid;
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 7386f4d635..069c0ebbee 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2407,7 +2407,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2484,7 +2484,147 @@ DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
-DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
-DETAIL: drop cascades to table fkpart10.tbl1
-drop cascades to table fkpart10.tbl2
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart11.pk VALUES (1), (3);
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart11.pk12 | 2
+ fkpart11.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a
+---------------+---
+ fkpart11.pk11 | 1
+ fkpart11.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 67aa20435d..aa05bfa62b 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1753,4 +1753,87 @@ DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
-DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart11.pk VALUES (1), (3);
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
v6-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v6-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From 2e8a275d13e000a79245e44a580ba1b6dd2c2198 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v6 1/3] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 395 +++++++++++++++++++++----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 20 +-
src/bin/psql/describe.c | 9 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 4 +-
6 files changed, 453 insertions(+), 80 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3349bcfaa7..6c269e875e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -450,12 +450,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -463,15 +465,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -8965,7 +8982,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8978,7 +8996,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9012,13 +9031,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -9026,6 +9049,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9122,15 +9147,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9174,7 +9197,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9217,14 +9241,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9233,19 +9264,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9274,6 +9307,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9315,7 +9357,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9394,10 +9439,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9453,6 +9502,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9482,6 +9532,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9499,6 +9557,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9565,6 +9625,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9577,11 +9647,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9604,6 +9678,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9625,6 +9700,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9654,6 +9737,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9682,6 +9767,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents
+ * to the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9700,7 +9798,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9799,9 +9900,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9822,16 +9927,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -9892,12 +10001,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -9930,13 +10037,118 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10737,10 +10949,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10780,23 +11001,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10848,9 +11078,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10905,25 +11138,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17024,19 +17267,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17156,6 +17390,7 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
List *indexes;
List *fks;
ListCell *cell;
+ Relation trigrel = NULL;
/*
* We must lock the default partition, because detaching this partition
@@ -17248,12 +17483,16 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -17271,6 +17510,20 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -17285,11 +17538,15 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -17361,6 +17618,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because they will be when the foreign keys are themselves
+ * detached.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 4e4e05844c..1298a53e86 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -126,8 +126,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -178,6 +180,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -716,6 +719,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -742,17 +746,16 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -849,7 +852,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1218,6 +1221,82 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index da6cc054b0..337511d9cc 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8011,7 +8011,25 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 90000)
+ if (fout->remoteVersion >= 140000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ */
+ appendPQExpBuffer(query,
+ "SELECT tgname, "
+ "tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(oid, false) AS tgdef, "
+ "tgenabled, tableoid, oid "
+ "FROM pg_catalog.pg_trigger t "
+ "WHERE tgrelid = '%u'::pg_catalog.oid "
+ "AND NOT tgisinternal "
+ "AND tgparentid = 0",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 90000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index eeac0efc4f..0d209ed7ee 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3000,7 +3000,14 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+ /*
+ * tgisnternal is set to true for inherited triggers of partitions in
+ * the servers between v11 and v13, though still must be shown to the
+ * user. So we use another property that is true for such inherited
+ * triggers to avoid them being hidden, which is their dependendence
+ * on another trigger.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 140000)
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"
" AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..a46d2734c9 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -155,6 +155,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e8af9a9589..997ac6e951 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3255,7 +3255,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
On Tue, Mar 23, 2021 at 6:27 PM Amit Langote <amitlangote09@gmail.com> wrote:
Sawada-san,
On Wed, Mar 10, 2021 at 4:51 PM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
I looked at the 0001 patch and here are random comments. Please ignore
a comment if it is already discussed.Thanks a lot for the review and sorry for the delay in replying.
No problem. Sorry for the late reply too.
Or is this change really needed? This change added one condition
"tgparentid = 0" but IIUC I think triggers that are NOT tgisinternal
are always tgparentid = 0. Also, it seems it is true both before and
after this patch.Actually, as noted in the commit message, I'm intending to change
tgisnternal to only be true for triggers generated by foreign keys and
no longer for partitions' user-defined triggers that are inherited.
So whereas NOT tgisnternal would suffice to exclude partitions'
inherited triggers before, that would no longer be the case with this
patch; AND tgparentid = 0 will be needed for that.
Understood.
Actually, I found a big hole in my assumptions around deferrable
foreign constraints, invalidating the approach I took in 0002 to use a
query-lifetime tuplestore to record root parent tuples. I'm trying to
find a way to make the tuplestore transaction-lifetime so that the
patch still works.In the meantime, I'm attaching an updated set with 0001 changed per
your comments.
0001 patch conflicts with 71f4c8c6f74. Could you please rebase the patchset?
Regards,
--
Masahiko Sawada
EDB: https://www.enterprisedb.com/
On Thu, Apr 1, 2021 at 10:56 AM Masahiko Sawada <sawada.mshk@gmail.com> wrote:
On Tue, Mar 23, 2021 at 6:27 PM Amit Langote <amitlangote09@gmail.com> wrote:
Actually, I found a big hole in my assumptions around deferrable
foreign constraints, invalidating the approach I took in 0002 to use a
query-lifetime tuplestore to record root parent tuples. I'm trying to
find a way to make the tuplestore transaction-lifetime so that the
patch still works.In the meantime, I'm attaching an updated set with 0001 changed per
your comments.0001 patch conflicts with 71f4c8c6f74. Could you please rebase the patchset?
Thanks for the heads up.
I still don't have a working patch to address the above mentioned
shortcoming of the previous approach, but here is a rebased version in
the meantime.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v7-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v7-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From 8476c194f17c2c224c4f56fd101322abae802d73 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v7 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 395 +++++++++++++++++++++----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 20 +-
src/bin/psql/describe.c | 9 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 4 +-
6 files changed, 453 insertions(+), 80 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 88a68a4697..2916c894ef 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -465,12 +465,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -478,15 +480,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -9056,7 +9073,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9069,7 +9087,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9103,13 +9122,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -9117,6 +9140,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9213,15 +9238,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9265,7 +9288,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9308,14 +9332,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9324,19 +9355,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9365,6 +9398,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel, false);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9406,7 +9448,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9485,10 +9530,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9544,6 +9593,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9573,6 +9623,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9590,6 +9648,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9656,6 +9716,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9668,11 +9738,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9695,6 +9769,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9716,6 +9791,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9745,6 +9828,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9773,6 +9858,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents
+ * to the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9791,7 +9889,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9890,9 +9991,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9913,16 +10018,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -9983,12 +10092,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -10021,13 +10128,118 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10828,10 +11040,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10871,23 +11092,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10939,9 +11169,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10996,25 +11229,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17238,19 +17481,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17552,6 +17786,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ Relation trigrel = NULL;
if (concurrent)
{
@@ -17570,12 +17805,16 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -17593,6 +17832,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -17607,11 +17860,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -17822,6 +18079,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because they will be when the foreign keys are themselves
+ * detached.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index a5ceb1698c..8b0fedd7d4 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -126,8 +126,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -178,6 +180,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -716,6 +719,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -742,17 +746,16 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -849,7 +852,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1219,6 +1222,82 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 25717ce0e6..f90985782e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8101,7 +8101,25 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 90000)
+ if (fout->remoteVersion >= 140000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ */
+ appendPQExpBuffer(query,
+ "SELECT tgname, "
+ "tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(oid, false) AS tgdef, "
+ "tgenabled, tableoid, oid "
+ "FROM pg_catalog.pg_trigger t "
+ "WHERE tgrelid = '%u'::pg_catalog.oid "
+ "AND NOT tgisinternal "
+ "AND tgparentid = 0",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 90000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 440249ff69..d8f1375e9d 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3104,7 +3104,14 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+ /*
+ * tgisnternal is set to true for inherited triggers of partitions in
+ * the servers between v11 and v13, though still must be shown to the
+ * user. So we use another property that is true for such inherited
+ * triggers to avoid them being hidden, which is their dependendence
+ * on another trigger.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 140000)
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"
" AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..a46d2734c9 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -155,6 +155,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e8af9a9589..997ac6e951 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3255,7 +3255,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v7-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v7-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From a1605e9a5aa0f24c2bf4812a080f4cb499e4001b Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v7 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing of the delete triggers that implement those foreign keys can
result in surprising outcomes for those keys. For example, a given
foreign key's delete trigger which implements the ON DELETE CASCADE
clause of that key will delete any referencing rows, although it
should not, because the referenced row is simply being moved into
another partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
To make this possible, this adjusts AFTER trigger data strucutures
to allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 177 +++++++++++-----
src/backend/executor/execMain.c | 9 +
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 237 ++++++++++++++++++++--
src/backend/utils/adt/ri_triggers.c | 17 +-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 4 +
src/test/regress/expected/foreign_key.out | 150 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 85 +++++++-
10 files changed, 616 insertions(+), 76 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 8b0fedd7d4..576b65cee5 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -88,13 +88,18 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static bool SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel);
/*
@@ -2308,7 +2313,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2397,7 +2402,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2522,7 +2527,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2619,7 +2624,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2643,7 +2649,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2764,7 +2770,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -2903,7 +2909,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2938,7 +2945,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3064,7 +3071,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3352,19 +3359,21 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. For partitioned tables, we never
+ * queue any events for its deferred triggers. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3385,8 +3394,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3580,7 +3589,8 @@ typedef struct AfterTriggersData
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3631,15 +3641,15 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
+ * Get the tuplestore for the current trigger query level, creating it
* if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetCurrentAfterTriggerTuplestore(void)
{
Tuplestorestate *ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
+ ret = afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
if (ret == NULL)
{
MemoryContext oldcxt;
@@ -3658,7 +3668,7 @@ GetCurrentFDWTuplestore(void)
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
+ afterTriggers.query_stack[afterTriggers.query_depth].tuplestore = ret;
}
return ret;
@@ -3992,22 +4002,22 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore = GetCurrentAfterTriggerTuplestore();
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4308,7 +4318,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4717,8 +4728,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5052,7 +5063,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5523,7 +5534,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5537,7 +5549,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5720,7 +5732,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5740,16 +5754,22 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ SkipCrossPartitionUpdateFKeyTrigger(mtstate, trigger, event, rel))
+ continue;
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ if (tuplestore == NULL)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ tuplestore = GetCurrentAfterTriggerTuplestore();
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5823,17 +5843,70 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(tuplestore, newslot);
+ }
+}
+
+/*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+static bool
+SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel)
+{
+ Relation rootRelDesc = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (rootRelDesc->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ return false;
+
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ return true;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ return true;
+ break;
}
+
+ return false;
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 163242f54e..aa9524e000 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1427,8 +1427,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 1e285e0349..9ae702c5cb 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index bf65785e64..6616c65841 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -463,7 +464,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -790,7 +793,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -828,6 +831,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1186,7 +1194,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1201,7 +1209,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1273,7 +1281,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
@@ -1371,8 +1381,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1385,6 +1396,180 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up trigger events necessary to check that a cross-partition update
+ * of the target partitioned table hasn't broken any foreign keys pointing
+ * to it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ HeapTuple oldtuple;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ Relation sourcePartRelDesc = sourcePartInfo->ri_RelationDesc;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo, mtstate);
+
+ /*
+ * There better not be any foreign keys that point directly to a non-root
+ * ancestor of the target source partition, because we can't enforce them.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted "new" tuple into the root table's slot, after
+ * converting it if needed.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(sourcePartInfo, mtstate);
+ if (newslot != oldslot && map)
+ newslot = execute_attr_map_slot(map->attrMap, newslot, rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(sourcePartRelDesc, tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1543,9 +1728,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1557,14 +1745,38 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1739,7 +1951,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2383,7 +2596,7 @@ ExecModifyTable(PlanState *pstate)
case CMD_INSERT:
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 7c77c338ce..6df2a93e8b 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1267,11 +1267,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index a46d2734c9..e92c349ad3 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -206,6 +206,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -225,6 +226,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 52d1fa018b..dac47c5390 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -516,9 +516,13 @@ typedef struct ResultRelInfo
* transition tuple capture or update partition row movement is active.
*/
TupleConversionMap *ri_ChildToRootMap;
+ bool ri_ChildToRootMapValid;
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 7386f4d635..069c0ebbee 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2407,7 +2407,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2484,7 +2484,147 @@ DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
-DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
-DETAIL: drop cascades to table fkpart10.tbl1
-drop cascades to table fkpart10.tbl2
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart11.pk VALUES (1), (3);
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart11.pk12 | 2
+ fkpart11.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a
+---------------+---
+ fkpart11.pk11 | 1
+ fkpart11.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 67aa20435d..aa05bfa62b 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1753,4 +1753,87 @@ DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
-DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart11.pk VALUES (1), (3);
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
On Fri, Apr 2, 2021 at 6:09 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Thu, Apr 1, 2021 at 10:56 AM Masahiko Sawada <sawada.mshk@gmail.com>
wrote:On Tue, Mar 23, 2021 at 6:27 PM Amit Langote <amitlangote09@gmail.com>
wrote:
Actually, I found a big hole in my assumptions around deferrable
foreign constraints, invalidating the approach I took in 0002 to use a
query-lifetime tuplestore to record root parent tuples. I'm trying to
find a way to make the tuplestore transaction-lifetime so that the
patch still works.In the meantime, I'm attaching an updated set with 0001 changed per
your comments.0001 patch conflicts with 71f4c8c6f74. Could you please rebase the
patchset?
Thanks for the heads up.
I still don't have a working patch to address the above mentioned
shortcoming of the previous approach, but here is a rebased version in
the meantime.--
Amit Langote
EDB: http://www.enterprisedb.com
@Amit patch is not successfully applying, can you please rebase that.
Masahiko Sawada, it's been a bit long since you reviewed the patch, are you
still interested to review that?
--
Ibrar Ahmed
Hi Ibrar, Sawada-san,
On Tue, Jul 13, 2021 at 20:25 Ibrar Ahmed <ibrar.ahmad@gmail.com> wrote:
On Fri, Apr 2, 2021 at 6:09 PM Amit Langote <amitlangote09@gmail.com>
wrote:On Thu, Apr 1, 2021 at 10:56 AM Masahiko Sawada <sawada.mshk@gmail.com>
wrote:On Tue, Mar 23, 2021 at 6:27 PM Amit Langote <amitlangote09@gmail.com>
wrote:
Actually, I found a big hole in my assumptions around deferrable
foreign constraints, invalidating the approach I took in 0002 to use a
query-lifetime tuplestore to record root parent tuples. I'm trying to
find a way to make the tuplestore transaction-lifetime so that the
patch still works.In the meantime, I'm attaching an updated set with 0001 changed per
your comments.0001 patch conflicts with 71f4c8c6f74. Could you please rebase the
patchset?
Thanks for the heads up.
I still don't have a working patch to address the above mentioned
shortcoming of the previous approach, but here is a rebased version in
the meantime.--
Amit Langote
EDB: http://www.enterprisedb.com@Amit patch is not successfully applying, can you please rebase that.
Thanks for the reminder.
Masahiko Sawada, it's been a bit long since you reviewed the patch, are you
still interested to review that?
Unfortunately, I don’t think I’ll have time in this CF to solve some very
fundamental issues I found in the patch during the last cycle. I’m fine
with either marking this as RwF for now or move to the next CF.
--
Amit Langote
EDB: http://www.enterprisedb.com
On 7/13/21 8:09 AM, Amit Langote wrote:
@Amit patch is not successfully applying, can you please rebase that.
Thanks for the reminder.
Masahiko Sawada, it's been a bit long since you reviewed the
patch, are you still interested to review that?Unfortunately, I don’t think I’ll have time in this CF to solve some
very fundamental issues I found in the patch during the last cycle.
I’m fine with either marking this as RwF for now or move to the next CF.
Amit, do you have time now to work on this?
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
Hi Andrew,
On Fri, Sep 3, 2021 at 6:19 AM Andrew Dunstan <andrew@dunslane.net> wrote:
On 7/13/21 8:09 AM, Amit Langote wrote:
Unfortunately, I don’t think I’ll have time in this CF to solve some
very fundamental issues I found in the patch during the last cycle.
I’m fine with either marking this as RwF for now or move to the next CF.Amit, do you have time now to work on this?
I will take some time next week to take a fresh look at this and post an update.
Thank you.
--
Amit Langote
EDB: http://www.enterprisedb.com
On Fri, Sep 3, 2021 at 12:23 PM Amit Langote <amitlangote09@gmail.com> wrote:
Hi Andrew,
On Fri, Sep 3, 2021 at 6:19 AM Andrew Dunstan <andrew@dunslane.net> wrote:
On 7/13/21 8:09 AM, Amit Langote wrote:
Unfortunately, I don’t think I’ll have time in this CF to solve some
very fundamental issues I found in the patch during the last cycle.
I’m fine with either marking this as RwF for now or move to the next CF.Amit, do you have time now to work on this?
I will take some time next week to take a fresh look at this and post an update.
So I started looking at this today. I didn't make much an inroad into
the stumbling block with 0002 patch that I had mentioned back in [1]/messages/by-id/CA+HiwqHMpNZOc2Z-zgdO9hbJ7wMCOC=WpJYszVusZ=oE2OTf8w@mail.gmail.com,
though I decided to at least post a rebased version of the patches
that apply.
I think 0001 is independently committable on its own merits,
irrespective of the yet unresolved problems of 0002, a patch to fix
$subject, which I'll continue to work on.
0003 shows a test that crashes the server due to said problem.
--
Amit Langote
EDB: http://www.enterprisedb.com
[1]: /messages/by-id/CA+HiwqHMpNZOc2Z-zgdO9hbJ7wMCOC=WpJYszVusZ=oE2OTf8w@mail.gmail.com
Attachments:
v8-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v8-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From db5686b1422c7cfe1442bc3cfa62af621d67c039 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v8 1/3] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 395 +++++++++++++++++++++----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 42 ++-
src/bin/pg_dump/pg_dump.h | 2 +-
src/bin/psql/describe.c | 9 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 4 +-
7 files changed, 468 insertions(+), 89 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index dbee6ae199..1386319fb6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -482,12 +482,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -495,15 +497,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -9304,7 +9321,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9317,7 +9335,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9351,13 +9370,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -9365,6 +9388,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9461,15 +9486,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9513,7 +9536,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9556,14 +9580,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9572,19 +9603,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9613,6 +9646,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9654,7 +9696,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9733,10 +9778,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9792,6 +9841,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9821,6 +9871,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9838,6 +9896,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9904,6 +9964,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9916,11 +9986,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9943,6 +10017,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9964,6 +10039,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9993,6 +10076,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -10021,6 +10106,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents
+ * to the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -10039,7 +10137,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -10138,9 +10239,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -10161,16 +10266,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -10231,12 +10340,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -10269,13 +10376,118 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -11192,10 +11404,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -11235,23 +11456,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -11303,9 +11533,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -11360,25 +11593,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17683,19 +17926,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17997,6 +18231,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ Relation trigrel = NULL;
if (concurrent)
{
@@ -18015,12 +18250,16 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -18038,6 +18277,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -18052,11 +18305,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -18262,6 +18519,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because they will be when the foreign keys are themselves
+ * detached.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index d8890d2c74..6e97285a30 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -132,8 +132,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -202,6 +204,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -741,6 +744,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -767,17 +771,16 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -874,7 +877,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when;
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1243,6 +1246,82 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2febcd4213..73a0e22ff2 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7966,7 +7966,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid,
i_tgconstrrelname,
i_tgenabled,
- i_tgisinternal,
+ i_tgispartition,
i_tgdeferrable,
i_tginitdeferred,
i_tgdef;
@@ -7985,21 +7985,42 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 130000)
+ if (fout->remoteVersion >= 150000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
* could result in non-forward-compatible dumps of WHEN clauses
* due to under-parenthesization.
*
+ * NB: We need to see partition triggers in case the tgenabled
+ * flag has been changed from the parent.
+ */
+ appendPQExpBuffer(query,
+ "SELECT t.tgname, "
+ "t.tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition "
+ "FROM pg_catalog.pg_trigger t "
+ "LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
+ "WHERE t.tgrelid = '%u'::pg_catalog.oid "
+ "AND (t.tgparentid = 0 OR t.tgenabled != u.tgenabled)",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 130000)
+ {
+ /*
* NB: We need to see tgisinternal triggers in partitions, in case
* the tgenabled flag has been changed from the parent.
+ *
+ * See above about pretty=true in pg_get_triggerdef.
*/
appendPQExpBuffer(query,
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition "
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
"WHERE t.tgrelid = '%u'::pg_catalog.oid "
@@ -8020,7 +8041,8 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "pt.oid = refobjid AS tgispartition "
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_depend AS d ON "
" d.classid = 'pg_catalog.pg_trigger'::pg_catalog.regclass AND "
@@ -8040,7 +8062,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, false as tgisinternal, "
+ "t.tgenabled, false as tgispartition, "
"t.tableoid, t.oid "
"FROM pg_catalog.pg_trigger t "
"WHERE tgrelid = '%u'::pg_catalog.oid "
@@ -8056,7 +8078,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT tgname, "
"tgfoid::pg_catalog.regproc AS tgfname, "
"tgtype, tgnargs, tgargs, tgenabled, "
- "false as tgisinternal, "
+ "false as tgispartition, "
"tgisconstraint, tgconstrname, tgdeferrable, "
"tgconstrrelid, tginitdeferred, tableoid, oid, "
"tgconstrrelid::pg_catalog.regclass AS tgconstrrelname "
@@ -8105,7 +8127,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid = PQfnumber(res, "tgconstrrelid");
i_tgconstrrelname = PQfnumber(res, "tgconstrrelname");
i_tgenabled = PQfnumber(res, "tgenabled");
- i_tgisinternal = PQfnumber(res, "tgisinternal");
+ i_tgispartition = PQfnumber(res, "tgispartition");
i_tgdeferrable = PQfnumber(res, "tgdeferrable");
i_tginitdeferred = PQfnumber(res, "tginitdeferred");
i_tgdef = PQfnumber(res, "tgdef");
@@ -8125,7 +8147,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tginfo[j].dobj.namespace = tbinfo->dobj.namespace;
tginfo[j].tgtable = tbinfo;
tginfo[j].tgenabled = *(PQgetvalue(res, j, i_tgenabled));
- tginfo[j].tgisinternal = *(PQgetvalue(res, j, i_tgisinternal)) == 't';
+ tginfo[j].tgispartition = *(PQgetvalue(res, j, i_tgispartition)) == 't';
if (i_tgdef >= 0)
{
tginfo[j].tgdef = pg_strdup(PQgetvalue(res, j, i_tgdef));
@@ -17841,8 +17863,10 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
"pg_catalog.pg_trigger", "TRIGGER",
trigidentity->data);
- if (tginfo->tgisinternal)
+ if (tginfo->tgispartition)
{
+ Assert(tbinfo->ispartition);
+
/*
* Triggers marked internal only appear here because their 'tgenabled'
* flag differs from its parent's. The trigger is created already, so
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 29af845ece..c7106d5744 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -427,7 +427,7 @@ typedef struct _triggerInfo
Oid tgconstrrelid;
char *tgconstrrelname;
char tgenabled;
- bool tgisinternal;
+ bool tgispartition;
bool tgdeferrable;
bool tginitdeferred;
char *tgdef;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 90ff649be7..7498597d17 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3266,7 +3266,14 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+ /*
+ * tgisnternal is set to true for inherited triggers of partitions in
+ * the servers between v11 and v13, though still must be shown to the
+ * user. So we use another property that is true for such inherited
+ * triggers to avoid them being hidden, which is their dependendence
+ * on another trigger.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 140000)
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"
" AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9ef7f6d768..8542705c5f 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -160,6 +160,10 @@ extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *que
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5d124cf96f..5c0e7c2b79 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3311,7 +3311,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v8-0003-This-test-crashes-the-patch.patchapplication/octet-stream; name=v8-0003-This-test-crashes-the-patch.patchDownload
From 6a52a3e3eb7ba1550c5fe66b88eca35442aa2400 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Thu, 11 Mar 2021 18:15:38 +0900
Subject: [PATCH v8 3/3] This test crashes the patch
---
src/test/regress/sql/foreign_key.sql | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 0ddc57c902..b7600524a5 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1828,6 +1828,8 @@ DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+DROP SCHEMA fkpart10 CASCADE;
-- verify foreign keys are enforced during cross-partition updates,
-- especially on the PK side
--
2.24.1
v8-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v8-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From fa3c50725a62e109be650b8485158cceeec65755 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v8 2/3] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing of the delete triggers that implement those foreign keys can
result in surprising outcomes for those keys. For example, a given
foreign key's delete trigger which implements the ON DELETE CASCADE
clause of that key will delete any referencing rows, although it
should not, because the referenced row is simply being moved into
another partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
To make this possible, this adjusts AFTER trigger data strucutures
to allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 177 +++++++++++-----
src/backend/executor/execMain.c | 9 +
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 237 ++++++++++++++++++++--
src/backend/utils/adt/ri_triggers.c | 17 +-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 150 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 85 +++++++-
10 files changed, 615 insertions(+), 76 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6e97285a30..4819935357 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,13 +94,18 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static bool SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel);
/*
@@ -2456,7 +2461,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2545,7 +2550,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2670,7 +2675,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2767,7 +2772,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2791,7 +2797,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2912,7 +2918,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3051,7 +3057,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -3086,7 +3093,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3212,7 +3219,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3500,19 +3507,21 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. For partitioned tables, we never
+ * queue any events for its deferred triggers. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3533,8 +3542,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3728,7 +3737,8 @@ typedef struct AfterTriggersData
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3779,15 +3789,15 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
+ * Get the tuplestore for the current trigger query level, creating it
* if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetCurrentAfterTriggerTuplestore(void)
{
Tuplestorestate *ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
+ ret = afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
if (ret == NULL)
{
MemoryContext oldcxt;
@@ -3806,7 +3816,7 @@ GetCurrentFDWTuplestore(void)
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
+ afterTriggers.query_stack[afterTriggers.query_depth].tuplestore = ret;
}
return ret;
@@ -4140,22 +4150,22 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore = GetCurrentAfterTriggerTuplestore();
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4458,7 +4468,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4867,8 +4878,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5202,7 +5213,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5673,7 +5684,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5687,7 +5699,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5870,7 +5882,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5890,16 +5904,22 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ SkipCrossPartitionUpdateFKeyTrigger(mtstate, trigger, event, rel))
+ continue;
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ if (tuplestore == NULL)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ tuplestore = GetCurrentAfterTriggerTuplestore();
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5973,17 +5993,70 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(tuplestore, newslot);
+ }
+}
+
+/*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+static bool
+SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel)
+{
+ Relation rootRelDesc = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (rootRelDesc->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ return false;
+
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ return true;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ return true;
+ break;
}
+
+ return false;
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..f7ce03bc14 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1447,8 +1447,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 574d7d27fd..747347b5bf 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index d328856ae5..84e99b2f3b 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -994,6 +997,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1354,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1369,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1441,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1566,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1581,180 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up trigger events necessary to check that a cross-partition update
+ * of the target partitioned table hasn't broken any foreign keys pointing
+ * to it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ HeapTuple oldtuple;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ Relation sourcePartRelDesc = sourcePartInfo->ri_RelationDesc;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo, mtstate);
+
+ /*
+ * There better not be any foreign keys that point directly to a non-root
+ * ancestor of the target source partition, because we can't enforce them.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted "new" tuple into the root table's slot, after
+ * converting it if needed.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(sourcePartInfo, mtstate);
+ if (newslot != oldslot && map)
+ newslot = execute_attr_map_slot(map->attrMap, newslot, rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(sourcePartRelDesc, tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1927,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1944,38 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2154,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2772,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 96269fc2ad..f04b2d87b6 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1267,11 +1267,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 8542705c5f..c10b850264 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,6 +231,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 37cb4f3d59..4dd55ae792 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -524,6 +524,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index bf794dce9d..3812b63748 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2485,7 +2485,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2562,7 +2562,147 @@ DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
-DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
-DETAIL: drop cascades to table fkpart10.tbl1
-drop cascades to table fkpart10.tbl2
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart11.pk VALUES (1), (3);
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart11.pk12 | 2
+ fkpart11.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a
+---------------+---
+ fkpart11.pk11 | 1
+ fkpart11.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index de417b62b6..0ddc57c902 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1828,4 +1828,87 @@ DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
-DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart11.pk VALUES (1), (3);
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
On Fri, Sep 10, 2021 at 7:06 AM Amit Langote <amitlangote09@gmail.com>
wrote:
On Fri, Sep 3, 2021 at 12:23 PM Amit Langote <amitlangote09@gmail.com>
wrote:Hi Andrew,
On Fri, Sep 3, 2021 at 6:19 AM Andrew Dunstan <andrew@dunslane.net>
wrote:
On 7/13/21 8:09 AM, Amit Langote wrote:
Unfortunately, I don’t think I’ll have time in this CF to solve some
very fundamental issues I found in the patch during the last cycle.
I’m fine with either marking this as RwF for now or move to the nextCF.
Amit, do you have time now to work on this?
I will take some time next week to take a fresh look at this and post an
update.
So I started looking at this today. I didn't make much an inroad into
the stumbling block with 0002 patch that I had mentioned back in [1],
though I decided to at least post a rebased version of the patches
that apply.I think 0001 is independently committable on its own merits,
irrespective of the yet unresolved problems of 0002, a patch to fix
$subject, which I'll continue to work on.0003 shows a test that crashes the server due to said problem.
--
Amit Langote
EDB: http://www.enterprisedb.com[1]
/messages/by-id/CA+HiwqHMpNZOc2Z-zgdO9hbJ7wMCOC=WpJYszVusZ=oE2OTf8w@mail.gmail.com
Hi,
For patch 0001, GetForeignKeyActionTriggers:
+ if (!OidIsValid(*deleteTriggerOid) || !OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find action triggers of foreign key
constraint %u",
I think if the error message includes whether it is the delete or update
trigger that isn't found, it would be helpful.
Similar comment for error message in GetForeignKeyCheckTriggers().
Cheers
On Fri, Sep 10, 2021 at 11:03 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Fri, Sep 3, 2021 at 12:23 PM Amit Langote <amitlangote09@gmail.com> wrote:
Hi Andrew,
On Fri, Sep 3, 2021 at 6:19 AM Andrew Dunstan <andrew@dunslane.net> wrote:
On 7/13/21 8:09 AM, Amit Langote wrote:
Unfortunately, I don’t think I’ll have time in this CF to solve some
very fundamental issues I found in the patch during the last cycle.
I’m fine with either marking this as RwF for now or move to the next CF.Amit, do you have time now to work on this?
I will take some time next week to take a fresh look at this and post an update.
So I started looking at this today. I didn't make much an inroad into
the stumbling block with 0002 patch that I had mentioned back in [1],
though I decided to at least post a rebased version of the patches
that apply.I think 0001 is independently committable on its own merits,
irrespective of the yet unresolved problems of 0002, a patch to fix
$subject, which I'll continue to work on.0003 shows a test that crashes the server due to said problem.
I think I found a solution to the problem with 0002.
The problem was that the tuplestore
(afterTriggers.query_stack[query_level].tuplestore) that I decided to
use to store the AFTER trigger tuples of a partitioned table that is
the target of an cross-partition update lives only for the duration of
a given query. So that approach wouldn't work if the foreign key
pointing into that partitioned table is marked INITIALLY DEFERRED. To
fix, I added a List field to AfterTriggersData that stores the
tuplestores to store the tuples of partitioned tables that undergo
cross-partition updates in a transaction and are pointed to by
INITIALLY DEFERRED foreign key constraints. I couldn't understand one
comment about why using a tuplestore for such cases *might not* work,
which as follows:
* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
* the right order. To ensure that, we forbid deferrable (constraint)
* triggers on foreign tables. This also ensures that such triggers do not
* get deferred into outer trigger query levels, meaning that it's okay to
* destroy the tuplestore at the end of the query level.
I tried to break the approach using various test cases (some can be
seen added by the patch to foreign_key.sql), but couldn't see the
issue alluded to in the above comment. So I've marked the comment
with an XXX note as follows:
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
+ * XXX - update this paragraph if the new approach, whereby tuplestores in
+ * afterTriggers.deferred_tuplestores outlive any given query, can be proven
+ * to not really break any assumptions mentioned here.
If anyone reading this can think of the issue the original comment
seems to be talking about, please let me know.
I've attached updated patches. I've addressed Zhihong Yu's comment too.
Thank you.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v9-0001-Create-foreign-key-triggers-in-partitioned-tables.patchapplication/octet-stream; name=v9-0001-Create-foreign-key-triggers-in-partitioned-tables.patchDownload
From 4c72a9e9aa1e0ee3ee4020d2689df3999b4d148b Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v9 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 401 +++++++++++++++++++++----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 45 ++-
src/bin/pg_dump/pg_dump.h | 2 +-
src/bin/psql/describe.c | 9 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 4 +-
7 files changed, 476 insertions(+), 90 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index dbee6ae199..a90b0486f4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -482,12 +482,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -495,15 +497,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -9304,7 +9321,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9317,7 +9335,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9351,13 +9370,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -9365,6 +9388,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9461,15 +9486,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9513,7 +9536,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9556,14 +9580,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9572,19 +9603,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9613,6 +9646,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9654,7 +9696,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9733,10 +9778,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9792,6 +9841,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9821,6 +9871,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9838,6 +9896,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9904,6 +9964,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9916,11 +9986,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9943,6 +10017,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9964,6 +10039,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9993,6 +10076,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -10021,6 +10106,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents
+ * to the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -10039,7 +10137,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -10138,9 +10239,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -10161,16 +10266,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -10231,12 +10340,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -10269,13 +10376,124 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid))
+ elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid))
+ elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -11192,10 +11410,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -11235,23 +11462,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -11303,9 +11539,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -11360,25 +11599,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17683,19 +17932,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17997,6 +18237,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ Relation trigrel = NULL;
if (concurrent)
{
@@ -18015,12 +18256,16 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -18038,6 +18283,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -18052,11 +18311,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -18262,6 +18525,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because they will be when the foreign keys are themselves
+ * detached.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index d8890d2c74..6e97285a30 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -132,8 +132,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -202,6 +204,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -741,6 +744,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -767,17 +771,16 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -874,7 +877,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when;
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1243,6 +1246,82 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a485fb2d07..c336f4448a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7972,7 +7972,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid,
i_tgconstrrelname,
i_tgenabled,
- i_tgisinternal,
+ i_tgispartition,
i_tgdeferrable,
i_tginitdeferred,
i_tgdef;
@@ -7991,21 +7991,43 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 130000)
+ if (fout->remoteVersion >= 150000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
* could result in non-forward-compatible dumps of WHEN clauses
* due to under-parenthesization.
*
+ * NB: We need to see partition triggers in case the tgenabled
+ * flag has been changed from the parent.
+ */
+ appendPQExpBuffer(query,
+ "SELECT t.tgname, "
+ "t.tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition "
+ "FROM pg_catalog.pg_trigger t "
+ "LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
+ "WHERE t.tgrelid = '%u'::pg_catalog.oid "
+ "AND ((NOT t.tgisinternal AND t.tgparentid = 0) "
+ "OR t.tgenabled != u.tgenabled)",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 130000)
+ {
+ /*
* NB: We need to see tgisinternal triggers in partitions, in case
* the tgenabled flag has been changed from the parent.
+ *
+ * See above about pretty=true in pg_get_triggerdef.
*/
appendPQExpBuffer(query,
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition "
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
"WHERE t.tgrelid = '%u'::pg_catalog.oid "
@@ -8026,7 +8048,8 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "pt.oid = refobjid AS tgispartition "
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_depend AS d ON "
" d.classid = 'pg_catalog.pg_trigger'::pg_catalog.regclass AND "
@@ -8046,7 +8069,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, false as tgisinternal, "
+ "t.tgenabled, false as tgispartition, "
"t.tableoid, t.oid "
"FROM pg_catalog.pg_trigger t "
"WHERE tgrelid = '%u'::pg_catalog.oid "
@@ -8062,7 +8085,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT tgname, "
"tgfoid::pg_catalog.regproc AS tgfname, "
"tgtype, tgnargs, tgargs, tgenabled, "
- "false as tgisinternal, "
+ "false as tgispartition, "
"tgisconstraint, tgconstrname, tgdeferrable, "
"tgconstrrelid, tginitdeferred, tableoid, oid, "
"tgconstrrelid::pg_catalog.regclass AS tgconstrrelname "
@@ -8111,7 +8134,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid = PQfnumber(res, "tgconstrrelid");
i_tgconstrrelname = PQfnumber(res, "tgconstrrelname");
i_tgenabled = PQfnumber(res, "tgenabled");
- i_tgisinternal = PQfnumber(res, "tgisinternal");
+ i_tgispartition = PQfnumber(res, "tgispartition");
i_tgdeferrable = PQfnumber(res, "tgdeferrable");
i_tginitdeferred = PQfnumber(res, "tginitdeferred");
i_tgdef = PQfnumber(res, "tgdef");
@@ -8131,7 +8154,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tginfo[j].dobj.namespace = tbinfo->dobj.namespace;
tginfo[j].tgtable = tbinfo;
tginfo[j].tgenabled = *(PQgetvalue(res, j, i_tgenabled));
- tginfo[j].tgisinternal = *(PQgetvalue(res, j, i_tgisinternal)) == 't';
+ tginfo[j].tgispartition = *(PQgetvalue(res, j, i_tgispartition)) == 't';
if (i_tgdef >= 0)
{
tginfo[j].tgdef = pg_strdup(PQgetvalue(res, j, i_tgdef));
@@ -17847,10 +17870,12 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
"pg_catalog.pg_trigger", "TRIGGER",
trigidentity->data);
- if (tginfo->tgisinternal)
+ if (tginfo->tgispartition)
{
+ Assert(tbinfo->ispartition);
+
/*
- * Triggers marked internal only appear here because their 'tgenabled'
+ * Partition triggers only appear here because their 'tgenabled'
* flag differs from its parent's. The trigger is created already, so
* remove the CREATE and replace it with an ALTER. (Clear out the
* DROP query too, so that pg_dump --create does not cause errors.)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 29af845ece..c7106d5744 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -427,7 +427,7 @@ typedef struct _triggerInfo
Oid tgconstrrelid;
char *tgconstrrelname;
char tgenabled;
- bool tgisinternal;
+ bool tgispartition;
bool tgdeferrable;
bool tginitdeferred;
char *tgdef;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 90ff649be7..7498597d17 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3266,7 +3266,14 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+ /*
+ * tgisnternal is set to true for inherited triggers of partitions in
+ * the servers between v11 and v13, though still must be shown to the
+ * user. So we use another property that is true for such inherited
+ * triggers to avoid them being hidden, which is their dependendence
+ * on another trigger.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 140000)
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"
" AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9ef7f6d768..8542705c5f 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -160,6 +160,10 @@ extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *que
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5d124cf96f..5c0e7c2b79 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3311,7 +3311,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v9-0002-Enforce-foreign-key-correctly-during-cross-partit.patchapplication/octet-stream; name=v9-0002-Enforce-foreign-key-correctly-during-cross-partit.patchDownload
From 484a29e63ff0935aaa50375012fee93f9839c439 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Fri, 13 Nov 2020 18:24:48 +0900
Subject: [PATCH v9 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing of the delete triggers that implement those foreign keys can
result in surprising outcomes for those keys. For example, a given
foreign key's delete trigger which implements the ON DELETE CASCADE
clause of that key will delete any referencing rows, although it
should not, because the referenced row is simply being moved into
another partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
To make this possible, this adjusts AFTER trigger data strucutures
to allow queuing and firing events containing partitioned table's
tuples. Given that partitioned tables are only logical relations,
meaning that its tuples have no physical identifiers, the only way
to remember the event tuples seems to be to store them in a
tuplestore, similar to what is currently done for foreign tables.
Although, for partitioned tables, these tuplestores may need to
outlive the query in which the trigger event was queued. For example,
if the foreign key pointing to the partitioned table is marked
INITIALLY DEFERRED, the tuples must be remembered untill transaction
ends. That is ensured by allocating the tuplestore under
TopTranscationContext and making TopTransactionResOwner its owner.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 305 +++++++++++++++++-----
src/backend/executor/execMain.c | 9 +
src/backend/executor/execReplication.c | 4 +-
src/backend/executor/nodeModifyTable.c | 237 ++++++++++++++++-
src/backend/utils/adt/ri_triggers.c | 17 +-
src/include/commands/trigger.h | 2 +
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 201 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 132 +++++++++-
10 files changed, 826 insertions(+), 91 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6e97285a30..3b1fdefdd1 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,13 +94,18 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
+static bool SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel);
/*
@@ -2456,7 +2461,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2545,7 +2550,7 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2670,7 +2675,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2767,7 +2772,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2791,7 +2797,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2912,7 +2918,7 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3051,7 +3057,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -3086,7 +3093,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo, TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3212,7 +3219,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo, TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3500,19 +3507,23 @@ typedef SetConstraintStateData *SetConstraintState;
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
- * tables use one; foreign tables always use zero and save the tuple(s) to a
- * tuplestore. AFTER_TRIGGER_FDW_FETCH directs AfterTriggerExecute() to
- * retrieve a fresh tuple or pair of tuples from that tuplestore, while
- * AFTER_TRIGGER_FDW_REUSE directs it to use the most-recently-retrieved
- * tuple(s). This permits storing tuples once regardless of the number of
- * row-level triggers on a foreign table.
+ * tables use one; foreign or partitioned tables always use zero and save the
+ * tuple(s) to a tuplestore. AFTER_TRIGGER_TS_FETCH directs
+ * AfterTriggerExecute() to retrieve a fresh tuple or pair of tuples from that
+ * tuplestore, while AFTER_TRIGGER_TS_REUSE directs it to use the
+ * most-recently-retrieved tuple(s). This permits storing tuples once
+ * regardless of the number of row-level triggers on a foreign or partitioned
+ * table.
*
- * Note that we need triggers on foreign tables to be fired in exactly the
- * order they were queued, so that the tuples come out of the tuplestore in
- * the right order. To ensure that, we forbid deferrable (constraint)
- * triggers on foreign tables. This also ensures that such triggers do not
- * get deferred into outer trigger query levels, meaning that it's okay to
- * destroy the tuplestore at the end of the query level.
+ * Note that we need triggers on foreign and partitioned tables to be fired in
+ * exactly the order they were queued, so that the tuples come out of the
+ * tuplestore in the right order. To ensure that, we forbid deferrable
+ * (constraint) triggers on foreign tables. This also ensures that such
+ * triggers do not get deferred into outer trigger query levels, meaning that
+ * it's okay to destroy the tuplestore at the end of the query level.
+ * XXX - update this paragraph if the new approach, whereby tuplestores in
+ * afterTriggers.deferred_tuplestores outlive any given query, can be proven
+ * to not really break any assumptions mentioned here.
*
* Statement-level triggers always bear AFTER_TRIGGER_1CTID, though they
* require no ctid field. We lack the flag bit space to neatly represent that
@@ -3533,8 +3544,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
-#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
+#define AFTER_TRIGGER_TS_REUSE 0x00000000
+#define AFTER_TRIGGER_TS_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
#define AFTER_TRIGGER_TUP_BITS 0xC0000000
@@ -3549,6 +3560,8 @@ typedef struct AfterTriggerSharedData
CommandId ats_firing_id; /* ID for firing cycle */
struct AfterTriggersTableData *ats_table; /* transition table access */
Bitmapset *ats_modifiedcols; /* modified columns */
+ Tuplestorestate *tuplestore; /* set if relation is a foreign or
+ * a partitioned table */
} AfterTriggerSharedData;
typedef struct AfterTriggerEventData *AfterTriggerEvent;
@@ -3659,11 +3672,12 @@ typedef struct AfterTriggerEventList
* occurs. At that point we fire immediate-mode triggers, and append any
* deferred events to the main events list.
*
- * fdw_tuplestore is a tuplestore containing the foreign-table tuples
- * needed by events queued by the current query. (Note: we use just one
- * tuplestore even though more than one foreign table might be involved.
- * This is okay because tuplestores don't really care what's in the tuples
- * they store; but it's possible that someday it'd break.)
+ * tuplestore is a tuplestore containing the foreign or partitioned table
+ * tuples needed by events queued by the current query. (Note: we use just
+ * one tuplestore even though more than one foreign or partitioned table
+ * might be involved. This is okay because tuplestores don't really care
+ * what's in the tuples they store; but it's possible that someday it'd
+ * break.)
*
* tables is a List of AfterTriggersTableData structs for target tables
* of the current query (see below).
@@ -3703,10 +3717,16 @@ typedef struct AfterTriggerEventList
* That's sufficient lifespan because we don't allow transition tables to be
* used by deferrable triggers, so they only need to survive until
* AfterTriggerEndQuery.
+ *
+ * tuplestores stored in 'deferred_tuplestores' live in
+ * TopTransactionContext and owned by TopTransactionResourceOwner. They are
+ * used to store the tuples of partitioned tables that are the targets of
+ * any deferred constraint triggers that get fired during the transaction.
*/
typedef struct AfterTriggersQueryData AfterTriggersQueryData;
typedef struct AfterTriggersTransData AfterTriggersTransData;
typedef struct AfterTriggersTableData AfterTriggersTableData;
+typedef struct AfterTriggersTuplestoreData AfterTriggersTuplestoreData;
typedef struct AfterTriggersData
{
@@ -3723,12 +3743,16 @@ typedef struct AfterTriggersData
/* per-subtransaction-level data: */
AfterTriggersTransData *trans_stack; /* array of structs shown below */
int maxtransdepth; /* allocated len of above array */
+
+ /* transaction-lifetime data: */
+ List *deferred_tuplestores;
} AfterTriggersData;
struct AfterTriggersQueryData
{
AfterTriggerEventList events; /* events pending from this query */
- Tuplestorestate *fdw_tuplestore; /* foreign tuples for said events */
+ Tuplestorestate *tuplestore; /* foreign or partitioned table tuples for
+ * said events */
List *tables; /* list of AfterTriggersTableData, see below */
};
@@ -3757,6 +3781,18 @@ struct AfterTriggersTableData
static AfterTriggersData afterTriggers;
+/*
+ * For each partitioned tables whose deferrable constraint triggers get fired
+ * during the transaction.
+ *
+ * Instances of this are added to afterTriggers.deferred_tuplestores.
+ */
+struct AfterTriggersTuplestoreData
+{
+ Oid relid; /* target table's OID */
+ Tuplestorestate *tuplestore; /* to store target table's tuples */
+};
+
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
@@ -3779,37 +3815,74 @@ static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent);
/*
- * Get the FDW tuplestore for the current trigger query level, creating it
- * if necessary.
+ * Get the special tuplestore needed to store foreign table or partitioned
+ * table trigger tuples, creating it in the correct context if necessary.
*/
static Tuplestorestate *
-GetCurrentFDWTuplestore(void)
+GetAfterTriggersTuplestore(Relation rel, bool initdeferred)
{
- Tuplestorestate *ret;
+ MemoryContext oldcxt;
+ ResourceOwner saveResourceOwner;
+ Tuplestorestate **ret;
- ret = afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore;
- if (ret == NULL)
+ /*
+ * If the per-query tuplestore has been set, that's the one that must be
+ * used to all events triggered in this query.
+ */
+ if (afterTriggers.query_stack[afterTriggers.query_depth].tuplestore)
+ return afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
+
+ /*
+ * If the event must be remembered till transaction end, so must the
+ * tuplestore, so allocate under top-level transaction.
+ */
+ if (initdeferred)
{
- MemoryContext oldcxt;
- ResourceOwner saveResourceOwner;
+ ListCell *lc;
+ AfterTriggersTuplestoreData *tuplestore_data;
+
+ /*
+ * It's okay to use the same tuplestore for a given relation OID even
+ * if the triggering query may have changed.
+ */
+ foreach(lc, afterTriggers.deferred_tuplestores)
+ {
+ AfterTriggersTuplestoreData *tuplestore_data =
+ (AfterTriggersTuplestoreData *) lfirst(lc);
+
+ if (tuplestore_data->relid == RelationGetRelid(rel))
+ return tuplestore_data->tuplestore;
+ }
+
+ oldcxt = MemoryContextSwitchTo(TopTransactionContext);
+ saveResourceOwner = CurrentResourceOwner;
+ CurrentResourceOwner = TopTransactionResourceOwner;
+
+ tuplestore_data = palloc(sizeof(AfterTriggersTuplestoreData));
+ afterTriggers.deferred_tuplestores =
+ lappend(afterTriggers.deferred_tuplestores, tuplestore_data);
+ tuplestore_data->relid = RelationGetRelid(rel);
+ ret = &tuplestore_data->tuplestore;
+ }
+ else
+ {
+ ret = &afterTriggers.query_stack[afterTriggers.query_depth].tuplestore;
/*
* Make the tuplestore valid until end of subtransaction. We really
- * only need it until AfterTriggerEndQuery().
+ * only need it until AfterTriggerEndQuery() though.
*/
oldcxt = MemoryContextSwitchTo(CurTransactionContext);
saveResourceOwner = CurrentResourceOwner;
CurrentResourceOwner = CurTransactionResourceOwner;
+ }
- ret = tuplestore_begin_heap(false, false, work_mem);
+ *ret = tuplestore_begin_heap(false, false, work_mem);
- CurrentResourceOwner = saveResourceOwner;
- MemoryContextSwitchTo(oldcxt);
-
- afterTriggers.query_stack[afterTriggers.query_depth].fdw_tuplestore = ret;
- }
+ CurrentResourceOwner = saveResourceOwner;
+ MemoryContextSwitchTo(oldcxt);
- return ret;
+ return *ret;
}
/* ----------
@@ -4140,22 +4213,26 @@ AfterTriggerExecute(EState *estate,
*/
switch (event->ate_flags & AFTER_TRIGGER_TUP_BITS)
{
- case AFTER_TRIGGER_FDW_FETCH:
+ case AFTER_TRIGGER_TS_FETCH:
{
- Tuplestorestate *fdw_tuplestore = GetCurrentFDWTuplestore();
+ Tuplestorestate *tuplestore;
- if (!tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ if (evtshared->tuplestore == NULL)
+ elog(ERROR, "no tuplestore to fetch tuples for AFTER trigger");
+ tuplestore = evtshared->tuplestore;
+
+ if (!tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot1))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
if ((evtshared->ats_event & TRIGGER_EVENT_OPMASK) ==
TRIGGER_EVENT_UPDATE &&
- !tuplestore_gettupleslot(fdw_tuplestore, true, false,
+ !tuplestore_gettupleslot(tuplestore, true, false,
trig_tuple_slot2))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
}
/* fall through */
- case AFTER_TRIGGER_FDW_REUSE:
+ case AFTER_TRIGGER_TS_REUSE:
/*
* Store tuple in the slot so that tg_trigtuple does not reference
@@ -4458,7 +4535,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
ExecDropSingleTupleTableSlot(slot2);
slot1 = slot2 = NULL;
}
- if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
slot1 = MakeSingleTupleTableSlot(rel->rd_att,
&TTSOpsMinimalTuple);
@@ -4734,6 +4812,7 @@ AfterTriggerBeginXact(void)
Assert(afterTriggers.events.head == NULL);
Assert(afterTriggers.trans_stack == NULL);
Assert(afterTriggers.maxtransdepth == 0);
+ Assert(afterTriggers.deferred_tuplestores == NIL);
}
@@ -4867,8 +4946,8 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
afterTriggerFreeEventList(&qs->events);
/* Drop FDW tuplestore if any */
- ts = qs->fdw_tuplestore;
- qs->fdw_tuplestore = NULL;
+ ts = qs->tuplestore;
+ qs->tuplestore = NULL;
if (ts)
tuplestore_end(ts);
@@ -5005,6 +5084,7 @@ AfterTriggerEndXact(bool isCommit)
afterTriggers.query_stack = NULL;
afterTriggers.maxquerydepth = 0;
afterTriggers.state = NULL;
+ afterTriggers.deferred_tuplestores = NIL;
/* No more afterTriggers manipulation until next transaction starts. */
afterTriggers.query_depth = -1;
@@ -5202,7 +5282,7 @@ AfterTriggerEnlargeQueryState(void)
qs->events.head = NULL;
qs->events.tail = NULL;
qs->events.tailfree = NULL;
- qs->fdw_tuplestore = NULL;
+ qs->tuplestore = NULL;
qs->tables = NIL;
++init_depth;
@@ -5673,7 +5753,8 @@ AfterTriggerPendingOnRel(Oid relid)
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -5687,7 +5768,8 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
int tgtype_event;
int tgtype_level;
int i;
- Tuplestorestate *fdw_tuplestore = NULL;
+ Tuplestorestate *immediate_tuplestore = NULL;
+ Tuplestorestate *deferred_tuplestore = NULL;
/*
* Check state. We use a normal test not Assert because it is possible to
@@ -5870,7 +5952,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
- if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
+ if (!row_trigger ||
+ (relkind != RELKIND_FOREIGN_TABLE &&
+ relkind != RELKIND_PARTITIONED_TABLE))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
/* else, we'll initialize ate_flags for each trigger */
@@ -5880,6 +5964,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
+ Tuplestorestate *tuplestore = NULL;
if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
tgtype_level,
@@ -5890,16 +5975,37 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
modifiedCols, oldslot, newslot))
continue;
- if (relkind == RELKIND_FOREIGN_TABLE && row_trigger)
+ if (mtstate && mtstate->operation == CMD_UPDATE &&
+ SkipCrossPartitionUpdateFKeyTrigger(mtstate, trigger, event, rel))
+ continue;
+
+ if (row_trigger &&
+ (relkind == RELKIND_FOREIGN_TABLE ||
+ relkind == RELKIND_PARTITIONED_TABLE))
{
- if (fdw_tuplestore == NULL)
+ bool first;
+
+ if (trigger->tginitdeferred)
+ {
+ first = (deferred_tuplestore == NULL);
+ deferred_tuplestore = tuplestore =
+ GetAfterTriggersTuplestore(rel, true);
+ }
+ else
+ {
+ first = (immediate_tuplestore == NULL);
+ immediate_tuplestore = tuplestore =
+ GetAfterTriggersTuplestore(rel, false);
+ }
+
+ if (first)
{
- fdw_tuplestore = GetCurrentFDWTuplestore();
- new_event.ate_flags = AFTER_TRIGGER_FDW_FETCH;
+ new_event.ate_flags = AFTER_TRIGGER_TS_FETCH;
+ first = false;
}
else
/* subsequent event for the same tuple */
- new_event.ate_flags = AFTER_TRIGGER_FDW_REUSE;
+ new_event.ate_flags = AFTER_TRIGGER_TS_REUSE;
}
/*
@@ -5967,23 +6073,84 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
else
new_shared.ats_table = NULL;
new_shared.ats_modifiedcols = modifiedCols;
+ new_shared.tuplestore = tuplestore;
afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth].events,
&new_event, &new_shared);
}
/*
- * Finally, spool any foreign tuple(s). The tuplestore squashes them to
- * minimal tuples, so this loses any system columns. The executor lost
- * those columns before us, for an unrelated reason, so this is fine.
+ * Finally, spool any foreign or partitioned table tuple(s). The
+ * tuplestore squashes them to minimal tuples, so this loses any system
+ * columns. The executor lost those columns before us, for an unrelated
+ * reason, so this is fine.
*/
- if (fdw_tuplestore)
+ if (immediate_tuplestore)
+ {
+ if (oldslot != NULL)
+ tuplestore_puttupleslot(immediate_tuplestore, oldslot);
+ if (newslot != NULL)
+ tuplestore_puttupleslot(immediate_tuplestore, newslot);
+ }
+ if (deferred_tuplestore)
{
if (oldslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, oldslot);
+ tuplestore_puttupleslot(deferred_tuplestore, oldslot);
if (newslot != NULL)
- tuplestore_puttupleslot(fdw_tuplestore, newslot);
+ tuplestore_puttupleslot(deferred_tuplestore, newslot);
+ }
+}
+
+/*
+ * Some events fired during the UPDATEs of partitioned tables that
+ * are turned into DELETE+INSERT must be skipped.
+ */
+static bool
+SkipCrossPartitionUpdateFKeyTrigger(ModifyTableState *mtstate,
+ Trigger *trigger, int event,
+ Relation rel)
+{
+ Relation rootRelDesc = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (rootRelDesc->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ return false;
+
+ switch (RI_FKey_trigger_type(trigger->tgfoid))
+ {
+ /*
+ * For UPDATEs of partitioned PK table, skip the events fired
+ * by the DELETEs unless the constraint originates in the
+ * relation on which it is fired (!tgisclone), because the
+ * UPDATE event fired on the root (partitioned) target table
+ * will be queued instead.
+ */
+ case RI_TRIGGER_PK:
+ if (TRIGGER_FIRED_BY_DELETE(event) && trigger->tgisclone)
+ return true;
+ break;
+
+ /*
+ * Skip events on the root partitione table if: 1) it's the FK
+ * table, because the events fired on the destination leaf
+ * partition suffice to do the checks necessary to enforce
+ * the FK relationship, 2) the trigger is unrelated to foreign
+ * keys, because the instance of the trigger in the leaf
+ * partitions will be fired instead. In fact, proceeding with
+ * firing the event on the partitioned table can be unsafe in
+ * both cases. For (1), RI_FKey_check() can't handle being
+ * handed a partitioned table. For (2), the trigger may be
+ * a INITIALLY DEFERRED constraint trigger, for which we
+ * can't ensure the event's tuples will be accessible when
+ * the trigger is fired.
+ */
+ case RI_TRIGGER_FK:
+ case RI_TRIGGER_NONE:
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ return true;
+ break;
}
+
+ return false;
}
/*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..f7ce03bc14 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1447,8 +1447,17 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /* Only close those we opened in GetAncestorResultRels(). */
+ if (rInfo->ri_RangeTableIndex == 0)
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 574d7d27fd..747347b5bf 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,7 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +556,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index d328856ae5..84e99b2f3b 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,7 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, NULL,
NULL,
slot,
NULL,
@@ -994,6 +997,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1354,7 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1369,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1441,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1566,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1581,180 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Returns tuple table slot that the caller can use to store the tuples in the
+ * the root target relation's format, creating it if not already done.
+ */
+static TupleTableSlot *
+GetRootTupleSlot(ModifyTableState *mtstate)
+{
+ if (mtstate->mt_root_tuple_slot == NULL)
+ {
+ Relation rootrel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ mtstate->mt_root_tuple_slot = table_slot_create(rootrel, NULL);
+ }
+
+ return mtstate->mt_root_tuple_slot;
+}
+
+/*
+ * Returns a map to convert the tuples of a given leaf partition result
+ * relation into the tuples of the root target relation, creating it if not
+ * already done.
+ */
+static TupleConversionMap *
+GetChildToRootMap(ResultRelInfo *resultRelInfo, ModifyTableState *mtstate)
+{
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ Relation relation = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(relation),
+ RelationGetDescr(rootRel));
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo,
+ ModifyTableState *mtstate)
+{
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Relation rootRel = mtstate->rootResultRelInfo->ri_RelationDesc;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ Oid rootRelOid = RelationGetRelid(rootRel);
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use mtstate->rootResultRelInfo for the root relation. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /*
+ * Pass 0 for RangeTableIndex to distinguish the relations that
+ * are opened here.
+ */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, mtstate->rootResultRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up trigger events necessary to check that a cross-partition update
+ * of the target partitioned table hasn't broken any foreign keys pointing
+ * to it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ HeapTuple oldtuple;
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+ ResultRelInfo *rootInfo = mtstate->rootResultRelInfo;
+ Relation sourcePartRelDesc = sourcePartInfo->ri_RelationDesc;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo, mtstate);
+
+ /*
+ * There better not be any foreign keys that point directly to a non-root
+ * ancestor of the target source partition, because we can't enforce them.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == mtstate->rootResultRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootInfo->ri_RelationDesc))));
+ }
+
+ /*
+ * Copy the inserted "new" tuple into the root table's slot, after
+ * converting it if needed.
+ */
+ rootslot = GetRootTupleSlot(mtstate);
+ map = GetChildToRootMap(sourcePartInfo, mtstate);
+ if (newslot != oldslot && map)
+ newslot = execute_attr_map_slot(map->attrMap, newslot, rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+
+ /* Get "old" HeapTuple from the source partition. */
+ if (!table_tuple_fetch_row_version(sourcePartRelDesc, tupleid,
+ SnapshotAny, oldslot))
+ elog(ERROR, "failed to fetch old tuple from source partition");
+ oldtuple = ExecFetchSlotHeapTuple(oldslot, true, NULL);
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootInfo, NULL, oldtuple,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1927,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1944,38 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2154,8 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2772,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 96269fc2ad..f04b2d87b6 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1267,11 +1267,20 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
* not do anything; so we had better do the UPDATE check. (We could skip
* this if we knew the INSERT trigger already fired, but there is no easy
* way to know that.)
+ *
+ * Skip the check and just ask to fire the trigger if the FK relation is
+ * a partitioned table, because we can't inspect system columns of the
+ * tuple in that case.
*/
- xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
- Assert(!isnull);
- xmin = DatumGetTransactionId(xminDatum);
- if (TransactionIdIsCurrentTransactionId(xmin))
+ if (fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull);
+ Assert(!isnull);
+ xmin = DatumGetTransactionId(xminDatum);
+ if (TransactionIdIsCurrentTransactionId(xmin))
+ return true;
+ }
+ else
return true;
/* If all old and new key values are equal, no check is needed */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 8542705c5f..c10b850264 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,6 +231,7 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 37cb4f3d59..4dd55ae792 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -524,6 +524,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index bf794dce9d..6b5c2d8ac3 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2485,7 +2485,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2554,15 +2554,210 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart11.pk VALUES (1), (3);
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a
+---------------+---
+ fkpart11.pk12 | 2
+ fkpart11.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when non-root ancestor is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a
+---------------+---
+ fkpart11.pk11 | 1
+ fkpart11.pk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index de417b62b6..88df1d791e 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1820,12 +1820,142 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk11 PARTITION OF pk1 FOR VALUES IN (1)
+ CREATE TABLE pk12 PARTITION OF pk1 FOR VALUES IN (2)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+INSERT INTO fkpart11.pk VALUES (1), (3);
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
On Mon, Sep 20, 2021 at 3:32 PM Amit Langote <amitlangote09@gmail.com> wrote:
The problem was that the tuplestore
(afterTriggers.query_stack[query_level].tuplestore) that I decided to
use to store the AFTER trigger tuples of a partitioned table that is
the target of an cross-partition update lives only for the duration of
a given query. So that approach wouldn't work if the foreign key
pointing into that partitioned table is marked INITIALLY DEFERRED. To
fix, I added a List field to AfterTriggersData that stores the
tuplestores to store the tuples of partitioned tables that undergo
cross-partition updates in a transaction and are pointed to by
INITIALLY DEFERRED foreign key constraints. I couldn't understand one
comment about why using a tuplestore for such cases *might not* work,
which as follows:* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
* the right order. To ensure that, we forbid deferrable (constraint)
* triggers on foreign tables. This also ensures that such triggers do not
* get deferred into outer trigger query levels, meaning that it's okay to
* destroy the tuplestore at the end of the query level.I tried to break the approach using various test cases (some can be
seen added by the patch to foreign_key.sql), but couldn't see the
issue alluded to in the above comment. So I've marked the comment
with an XXX note as follows:- * Note that we need triggers on foreign tables to be fired in exactly the - * order they were queued, so that the tuples come out of the tuplestore in - * the right order. To ensure that, we forbid deferrable (constraint) - * triggers on foreign tables. This also ensures that such triggers do not - * get deferred into outer trigger query levels, meaning that it's okay to - * destroy the tuplestore at the end of the query level. + * Note that we need triggers on foreign and partitioned tables to be fired in + * exactly the order they were queued, so that the tuples come out of the + * tuplestore in the right order. To ensure that, we forbid deferrable + * (constraint) triggers on foreign tables. This also ensures that such + * triggers do not get deferred into outer trigger query levels, meaning that + * it's okay to destroy the tuplestore at the end of the query level. + * XXX - update this paragraph if the new approach, whereby tuplestores in + * afterTriggers.deferred_tuplestores outlive any given query, can be proven + * to not really break any assumptions mentioned here.If anyone reading this can think of the issue the original comment
seems to be talking about, please let me know.
I brought this up in an off-list discussion with Robert and he asked
why I hadn't considered not using tuplestores to remember the tuples
in the first place, specifically pointing out that it may lead to
unnecessarily consuming a lot of memory when such updates move a bunch
of rows around. Like, we could simply remember the tuples to be
passed to the trigger function using their CTIDs as is done for normal
(single-heap-relation) updates, though in this case also remember the
OIDs of the source and the destination partitions of a particular
cross-partition update.
I had considered that idea before but I think I had overestimated the
complexity of that approach so didn't go with it. I tried and the
resulting patch doesn't look all that complicated, with the added
bonus that the bulk update case shouldn't consume so much memory with
this approach like the previous tuplestore version would have.
Updated patches attached.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v10-0001-Create-foreign-key-triggers-in-partitioned-table.patchapplication/octet-stream; name=v10-0001-Create-foreign-key-triggers-in-partitioned-table.patchDownload
From d8a19aa9e5f5dd86de61ea837db5bfab9e7ada07 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v10 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 401 +++++++++++++++++++++----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 45 ++-
src/bin/pg_dump/pg_dump.h | 2 +-
src/bin/psql/describe.c | 9 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 4 +-
7 files changed, 476 insertions(+), 90 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1c2ebe1bf6..ac3ea498e3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -482,12 +482,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -495,15 +497,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -9304,7 +9321,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9317,7 +9335,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9351,13 +9370,17 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -9365,6 +9388,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9461,15 +9486,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9513,7 +9536,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9556,14 +9580,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9572,19 +9603,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9613,6 +9646,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9654,7 +9696,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9733,10 +9778,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9792,6 +9841,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9821,6 +9871,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9838,6 +9896,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9904,6 +9964,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9916,11 +9986,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9943,6 +10017,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9964,6 +10039,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9993,6 +10076,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -10021,6 +10106,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents
+ * to the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -10039,7 +10137,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -10138,9 +10239,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -10161,16 +10266,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -10231,12 +10340,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -10269,13 +10376,124 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid))
+ elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid))
+ elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -11192,10 +11410,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -11235,23 +11462,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -11303,9 +11539,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -11360,25 +11599,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17683,19 +17932,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17997,6 +18237,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ Relation trigrel = NULL;
if (concurrent)
{
@@ -18015,12 +18256,16 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -18038,6 +18283,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -18052,11 +18311,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -18262,6 +18525,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because they will be when the foreign keys are themselves
+ * detached.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index d8890d2c74..6e97285a30 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -132,8 +132,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -202,6 +204,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -741,6 +744,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -767,17 +771,16 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -874,7 +877,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when;
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1243,6 +1246,82 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index a485fb2d07..c336f4448a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7972,7 +7972,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid,
i_tgconstrrelname,
i_tgenabled,
- i_tgisinternal,
+ i_tgispartition,
i_tgdeferrable,
i_tginitdeferred,
i_tgdef;
@@ -7991,21 +7991,43 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tbinfo->dobj.name);
resetPQExpBuffer(query);
- if (fout->remoteVersion >= 130000)
+ if (fout->remoteVersion >= 150000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It
* could result in non-forward-compatible dumps of WHEN clauses
* due to under-parenthesization.
*
+ * NB: We need to see partition triggers in case the tgenabled
+ * flag has been changed from the parent.
+ */
+ appendPQExpBuffer(query,
+ "SELECT t.tgname, "
+ "t.tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition "
+ "FROM pg_catalog.pg_trigger t "
+ "LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
+ "WHERE t.tgrelid = '%u'::pg_catalog.oid "
+ "AND ((NOT t.tgisinternal AND t.tgparentid = 0) "
+ "OR t.tgenabled != u.tgenabled)",
+ tbinfo->dobj.catId.oid);
+ }
+ else if (fout->remoteVersion >= 130000)
+ {
+ /*
* NB: We need to see tgisinternal triggers in partitions, in case
* the tgenabled flag has been changed from the parent.
+ *
+ * See above about pretty=true in pg_get_triggerdef.
*/
appendPQExpBuffer(query,
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition "
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
"WHERE t.tgrelid = '%u'::pg_catalog.oid "
@@ -8026,7 +8048,8 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "pt.oid = refobjid AS tgispartition "
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_depend AS d ON "
" d.classid = 'pg_catalog.pg_trigger'::pg_catalog.regclass AND "
@@ -8046,7 +8069,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, false as tgisinternal, "
+ "t.tgenabled, false as tgispartition, "
"t.tableoid, t.oid "
"FROM pg_catalog.pg_trigger t "
"WHERE tgrelid = '%u'::pg_catalog.oid "
@@ -8062,7 +8085,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
"SELECT tgname, "
"tgfoid::pg_catalog.regproc AS tgfname, "
"tgtype, tgnargs, tgargs, tgenabled, "
- "false as tgisinternal, "
+ "false as tgispartition, "
"tgisconstraint, tgconstrname, tgdeferrable, "
"tgconstrrelid, tginitdeferred, tableoid, oid, "
"tgconstrrelid::pg_catalog.regclass AS tgconstrrelname "
@@ -8111,7 +8134,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid = PQfnumber(res, "tgconstrrelid");
i_tgconstrrelname = PQfnumber(res, "tgconstrrelname");
i_tgenabled = PQfnumber(res, "tgenabled");
- i_tgisinternal = PQfnumber(res, "tgisinternal");
+ i_tgispartition = PQfnumber(res, "tgispartition");
i_tgdeferrable = PQfnumber(res, "tgdeferrable");
i_tginitdeferred = PQfnumber(res, "tginitdeferred");
i_tgdef = PQfnumber(res, "tgdef");
@@ -8131,7 +8154,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tginfo[j].dobj.namespace = tbinfo->dobj.namespace;
tginfo[j].tgtable = tbinfo;
tginfo[j].tgenabled = *(PQgetvalue(res, j, i_tgenabled));
- tginfo[j].tgisinternal = *(PQgetvalue(res, j, i_tgisinternal)) == 't';
+ tginfo[j].tgispartition = *(PQgetvalue(res, j, i_tgispartition)) == 't';
if (i_tgdef >= 0)
{
tginfo[j].tgdef = pg_strdup(PQgetvalue(res, j, i_tgdef));
@@ -17847,10 +17870,12 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
"pg_catalog.pg_trigger", "TRIGGER",
trigidentity->data);
- if (tginfo->tgisinternal)
+ if (tginfo->tgispartition)
{
+ Assert(tbinfo->ispartition);
+
/*
- * Triggers marked internal only appear here because their 'tgenabled'
+ * Partition triggers only appear here because their 'tgenabled'
* flag differs from its parent's. The trigger is created already, so
* remove the CREATE and replace it with an ALTER. (Clear out the
* DROP query too, so that pg_dump --create does not cause errors.)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 29af845ece..c7106d5744 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -427,7 +427,7 @@ typedef struct _triggerInfo
Oid tgconstrrelid;
char *tgconstrrelname;
char tgenabled;
- bool tgisinternal;
+ bool tgispartition;
bool tgdeferrable;
bool tginitdeferred;
char *tgdef;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index ea4ca5c05c..24ec7f7e44 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3265,7 +3265,14 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+ /*
+ * tgisnternal is set to true for inherited triggers of partitions in
+ * the servers between v11 and v13, though still must be shown to the
+ * user. So we use another property that is true for such inherited
+ * triggers to avoid them being hidden, which is their dependendence
+ * on another trigger.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 140000)
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"
" AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9ef7f6d768..8542705c5f 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -160,6 +160,10 @@ extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *que
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5d124cf96f..5c0e7c2b79 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3311,7 +3311,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v10-0002-Enforce-foreign-key-correctly-during-cross-parti.patchapplication/octet-stream; name=v10-0002-Enforce-foreign-key-correctly-during-cross-parti.patchDownload
From 070d0d7d8f0a00d861dbaecdb592389e1d6c554c Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH v10 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows, although it should not,
because the referenced row is simply being moved into another
partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 322 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 187 ++++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 4 +
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 135 ++++++++-
11 files changed, 840 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6e97285a30..74d71d4f82 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,7 +94,11 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2456,7 +2460,9 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2545,7 +2551,9 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2670,7 +2678,9 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2767,7 +2777,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2791,7 +2802,9 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2912,7 +2925,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3050,8 +3065,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
+ * destination partitions, respectively, of a cross-partition update of the
+ * root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey().
+ */
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -3071,12 +3098,15 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
+ src_partinfo != NULL ?
+ src_partinfo :
+ relinfo);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ src_partinfo != NULL ? src_partinfo : relinfo,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3086,7 +3116,9 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3212,7 +3244,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3529,7 +3563,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
@@ -3537,7 +3571,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_FDW_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0xC8000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -3558,8 +3593,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to
+ * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
+ ItemPointerData ate_ctid2; /* new updated tuple */
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3574,11 +3625,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3760,6 +3813,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4084,8 +4139,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4099,6 +4162,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4106,6 +4171,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4186,12 +4253,36 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4201,16 +4292,42 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4439,13 +4556,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4470,12 +4590,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5670,16 +5811,38 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
+ Relation rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
+ bool maybe_crosspart_update =
+ (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)));
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
@@ -5786,6 +5949,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partitioned update. In that
+ * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5846,6 +6022,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5872,11 +6058,43 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
+ AFTER_TRIGGER_2CTID) :
+ AFTER_TRIGGER_1CTID;
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the loop
+ * below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5906,12 +6124,28 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
* tell by inspection that the FK constraint will still pass.
+ * There are also some cases during cross-partition updates of a
+ * partitioned table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the update
+ * event that will be fired on the root (partitioned)
+ * target table will be used to perform the necessary
+ * foreign key enforcement action.
+ */
+ if (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5922,8 +6156,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5932,7 +6177,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a partitioned
+ * table, because the same row trigger must be present in
+ * the leaf partition(s) that are affected as part of this
+ * update and the events fired on them are queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..40b6d924ce 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 574d7d27fd..933b365cc5 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +557,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index d328856ae5..e9d1b3fb5d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,9 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
@@ -994,6 +999,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1356,8 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1372,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1444,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1569,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1584,124 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's trigger
+ * to check that a cross-partition update hasn't broken any foreign keys
+ * pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
+ sourcePartInfo, destPartInfo,
+ tupleid, NULL,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1874,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1891,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2102,10 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2722,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 96269fc2ad..38b3b9e53f 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1208,6 +1208,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 8542705c5f..2fd357f39d 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,7 +231,10 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index cd57a704ad..4abab5e4d0 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 37cb4f3d59..4dd55ae792 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -524,6 +524,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index bf794dce9d..6a0f499fcb 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2485,7 +2485,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2554,15 +2554,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index de417b62b6..6c69a69c7a 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1820,12 +1820,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
On Thu, Oct 14, 2021 at 6:00 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Mon, Sep 20, 2021 at 3:32 PM Amit Langote <amitlangote09@gmail.com> wrote:
The problem was that the tuplestore
(afterTriggers.query_stack[query_level].tuplestore) that I decided to
use to store the AFTER trigger tuples of a partitioned table that is
the target of an cross-partition update lives only for the duration of
a given query. So that approach wouldn't work if the foreign key
pointing into that partitioned table is marked INITIALLY DEFERRED. To
fix, I added a List field to AfterTriggersData that stores the
tuplestores to store the tuples of partitioned tables that undergo
cross-partition updates in a transaction and are pointed to by
INITIALLY DEFERRED foreign key constraints. I couldn't understand one
comment about why using a tuplestore for such cases *might not* work,
which as follows:* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
* the right order. To ensure that, we forbid deferrable (constraint)
* triggers on foreign tables. This also ensures that such triggers do not
* get deferred into outer trigger query levels, meaning that it's okay to
* destroy the tuplestore at the end of the query level.I tried to break the approach using various test cases (some can be
seen added by the patch to foreign_key.sql), but couldn't see the
issue alluded to in the above comment. So I've marked the comment
with an XXX note as follows:- * Note that we need triggers on foreign tables to be fired in exactly the - * order they were queued, so that the tuples come out of the tuplestore in - * the right order. To ensure that, we forbid deferrable (constraint) - * triggers on foreign tables. This also ensures that such triggers do not - * get deferred into outer trigger query levels, meaning that it's okay to - * destroy the tuplestore at the end of the query level. + * Note that we need triggers on foreign and partitioned tables to be fired in + * exactly the order they were queued, so that the tuples come out of the + * tuplestore in the right order. To ensure that, we forbid deferrable + * (constraint) triggers on foreign tables. This also ensures that such + * triggers do not get deferred into outer trigger query levels, meaning that + * it's okay to destroy the tuplestore at the end of the query level. + * XXX - update this paragraph if the new approach, whereby tuplestores in + * afterTriggers.deferred_tuplestores outlive any given query, can be proven + * to not really break any assumptions mentioned here.If anyone reading this can think of the issue the original comment
seems to be talking about, please let me know.I brought this up in an off-list discussion with Robert and he asked
why I hadn't considered not using tuplestores to remember the tuples
in the first place, specifically pointing out that it may lead to
unnecessarily consuming a lot of memory when such updates move a bunch
of rows around. Like, we could simply remember the tuples to be
passed to the trigger function using their CTIDs as is done for normal
(single-heap-relation) updates, though in this case also remember the
OIDs of the source and the destination partitions of a particular
cross-partition update.I had considered that idea before but I think I had overestimated the
complexity of that approach so didn't go with it. I tried and the
resulting patch doesn't look all that complicated, with the added
bonus that the bulk update case shouldn't consume so much memory with
this approach like the previous tuplestore version would have.Updated patches attached.
Patch 0001 conflicted with some pg_dump changes that were recently
committed, so rebased.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v11-0002-Enforce-foreign-key-correctly-during-cross-parti.patchapplication/octet-stream; name=v11-0002-Enforce-foreign-key-correctly-during-cross-parti.patchDownload
From 80bb090b82f17dc156a0351a93e5a135397c4fff Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH v11 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 322 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 187 ++++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 4 +
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 135 ++++++++-
11 files changed, 840 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 52b17294bf..2f3690e455 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,7 +94,11 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2458,7 +2462,9 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2547,7 +2553,9 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2672,7 +2680,9 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2769,7 +2779,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2793,7 +2804,9 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2914,7 +2927,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3052,8 +3067,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
+ * destination partitions, respectively, of a cross-partition update of the
+ * root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey().
+ */
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -3073,12 +3100,15 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
+ src_partinfo != NULL ?
+ src_partinfo :
+ relinfo);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ src_partinfo != NULL ? src_partinfo : relinfo,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,7 +3118,9 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3214,7 +3246,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3531,7 +3565,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
@@ -3539,7 +3573,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_FDW_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0xC8000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -3560,8 +3595,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to
+ * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
+ ItemPointerData ate_ctid2; /* new updated tuple */
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3576,11 +3627,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3815,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4086,8 +4141,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4101,6 +4164,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4108,6 +4173,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4188,12 +4255,36 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4203,16 +4294,42 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4441,13 +4558,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4472,12 +4592,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5672,16 +5813,38 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
+ Relation rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
+ bool maybe_crosspart_update =
+ (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)));
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
@@ -5788,6 +5951,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partitioned update. In that
+ * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5848,6 +6024,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5874,11 +6060,43 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
+ AFTER_TRIGGER_2CTID) :
+ AFTER_TRIGGER_1CTID;
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the loop
+ * below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5908,12 +6126,28 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
* tell by inspection that the FK constraint will still pass.
+ * There are also some cases during cross-partition updates of a
+ * partitioned table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the update
+ * event that will be fired on the root (partitioned)
+ * target table will be used to perform the necessary
+ * foreign key enforcement action.
+ */
+ if (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5924,8 +6158,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5934,7 +6179,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a partitioned
+ * table, because the same row trigger must be present in
+ * the leaf partition(s) that are affected as part of this
+ * update and the events fired on them are queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..40b6d924ce 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 574d7d27fd..933b365cc5 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +557,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index d328856ae5..e9d1b3fb5d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,9 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
@@ -994,6 +999,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1356,8 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1372,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1444,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1569,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1584,124 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's trigger
+ * to check that a cross-partition update hasn't broken any foreign keys
+ * pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
+ sourcePartInfo, destPartInfo,
+ tupleid, NULL,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1874,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1891,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2102,10 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2722,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 8ebb2a50a1..58f0115c01 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 8542705c5f..2fd357f39d 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,7 +231,10 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index cd57a704ad..4abab5e4d0 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index ddc3529332..6e5505f9d0 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -525,6 +525,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
v11-0001-Create-foreign-key-triggers-in-partitioned-table.patchapplication/octet-stream; name=v11-0001-Create-foreign-key-triggers-in-partitioned-table.patchDownload
From 61b61e1c6b3d60ea68683e83a8c4b90f1c3f05c3 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Tue, 10 Nov 2020 10:54:28 +0900
Subject: [PATCH v11 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
---
src/backend/commands/tablecmds.c | 401 +++++++++++++++++++++----
src/backend/commands/trigger.c | 101 ++++++-
src/bin/pg_dump/pg_dump.c | 38 ++-
src/bin/pg_dump/pg_dump.h | 2 +-
src/bin/psql/describe.c | 9 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 4 +-
7 files changed, 473 insertions(+), 86 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bf42587e38..8abd5d9746 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -483,7 +483,8 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
int numfksetcols, const int16 *fksetcolsattnums,
List *fksetcols);
@@ -492,7 +493,8 @@ static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -500,15 +502,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -9367,7 +9384,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ffeqoperators,
numfkdelsetcols,
fkdelsetcols,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9382,7 +9400,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
numfkdelsetcols,
fkdelsetcols,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9454,6 +9473,9 @@ validateFkOnDeleteSetColumns(int numfks, const int16 *fkattnums,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -9462,7 +9484,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok)
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -9470,6 +9493,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9568,15 +9593,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9621,7 +9644,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
numfkdelsetcols, fkdelsetcols,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9668,6 +9692,9 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
@@ -9675,8 +9702,12 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
int numfkdelsetcols, int16 *fkdelsetcols,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9685,19 +9716,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9726,6 +9759,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9767,7 +9809,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9850,10 +9895,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfkdelsetcols,
fkdelsetcols,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9909,6 +9958,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9938,6 +9988,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9957,6 +10015,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
int numfkdelsetcols;
AttrNumber confdelsetcols[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -10025,6 +10085,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -10039,11 +10109,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conffeqop,
numfkdelsetcols,
confdelsetcols,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -10066,6 +10140,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -10087,6 +10162,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -10118,6 +10201,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -10147,6 +10232,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents
+ * to the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -10165,7 +10263,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -10268,9 +10369,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfkdelsetcols,
confdelsetcols,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -10291,16 +10396,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -10361,12 +10470,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -10399,13 +10506,124 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ *deleteTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid))
+ elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ *insertTriggerOid = trgform->oid;
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ *updateTriggerOid = trgform->oid;
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid))
+ elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -11323,10 +11541,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -11366,23 +11593,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -11434,9 +11670,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress= CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -11491,25 +11730,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17862,19 +18111,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -18176,6 +18416,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ Relation trigrel = NULL;
if (concurrent)
{
@@ -18194,12 +18435,16 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -18217,6 +18462,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -18231,11 +18490,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -18460,6 +18723,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because they will be when the foreign keys are themselves
+ * detached.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7c8826089b..52b17294bf 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -132,8 +132,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -202,6 +204,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -741,6 +744,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -767,17 +771,16 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) && !isInternal &&
+ !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -876,7 +879,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when;
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1245,6 +1248,82 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index ac291fbef2..70f459e1d4 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7257,7 +7257,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid,
i_tgconstrrelname,
i_tgenabled,
- i_tgisinternal,
+ i_tgispartition,
i_tgdeferrable,
i_tginitdeferred,
i_tgdef;
@@ -7286,7 +7286,31 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
}
appendPQExpBufferChar(tbloids, '}');
- if (fout->remoteVersion >= 130000)
+ if (fout->remoteVersion >= 150000)
+ {
+ /*
+ * NB: think not to use pretty=true in pg_get_triggerdef. It
+ * could result in non-forward-compatible dumps of WHEN clauses
+ * due to under-parenthesization.
+ *
+ * NB: We need to see partition triggers in case the tgenabled
+ * flag has been changed from the parent.
+ */
+ appendPQExpBuffer(query,
+ "SELECT t.tgrelid, t.tgname, "
+ "t.tgfoid::pg_catalog.regproc AS tgfname, "
+ "pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition\n"
+ "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+ "JOIN pg_catalog.pg_trigger t ON (src.tbloid = t.tgrelid) "
+ "LEFT JOIN pg_catalog.pg_trigger u ON (u.oid = t.tgparentid) "
+ "WHERE ((NOT t.tgisinternal AND t.tgparentid = 0) "
+ "OR t.tgenabled != u.tgenabled) "
+ "ORDER BY t.tgrelid, t.tgname",
+ tbloids->data);
+ }
+ else if (fout->remoteVersion >= 130000)
{
/*
* NB: think not to use pretty=true in pg_get_triggerdef. It could
@@ -7366,7 +7390,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid = PQfnumber(res, "tgconstrrelid");
i_tgconstrrelname = PQfnumber(res, "tgconstrrelname");
i_tgenabled = PQfnumber(res, "tgenabled");
- i_tgisinternal = PQfnumber(res, "tgisinternal");
+ i_tgispartition = PQfnumber(res, "tgispartition");
i_tgdeferrable = PQfnumber(res, "tgdeferrable");
i_tginitdeferred = PQfnumber(res, "tginitdeferred");
i_tgdef = PQfnumber(res, "tgdef");
@@ -7416,7 +7440,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tginfo[j].dobj.namespace = tbinfo->dobj.namespace;
tginfo[j].tgtable = tbinfo;
tginfo[j].tgenabled = *(PQgetvalue(res, j, i_tgenabled));
- tginfo[j].tgisinternal = *(PQgetvalue(res, j, i_tgisinternal)) == 't';
+ tginfo[j].tgispartition = *(PQgetvalue(res, j, i_tgispartition)) == 't';
if (i_tgdef >= 0)
{
tginfo[j].tgdef = pg_strdup(PQgetvalue(res, j, i_tgdef));
@@ -16800,10 +16824,12 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
"pg_catalog.pg_trigger", "TRIGGER",
trigidentity->data);
- if (tginfo->tgisinternal)
+ if (tginfo->tgispartition)
{
+ Assert(tbinfo->ispartition);
+
/*
- * Triggers marked internal only appear here because their 'tgenabled'
+ * Partition triggers only appear here because their 'tgenabled'
* flag differs from its parent's. The trigger is created already, so
* remove the CREATE and replace it with an ALTER. (Clear out the
* DROP query too, so that pg_dump --create does not cause errors.)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index f011ace8a8..c6967f657d 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -443,7 +443,7 @@ typedef struct _triggerInfo
Oid tgconstrrelid;
char *tgconstrrelname;
char tgenabled;
- bool tgisinternal;
+ bool tgispartition;
bool tgdeferrable;
bool tginitdeferred;
char *tgdef;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c28788e84f..44cc1fa132 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3001,7 +3001,14 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+ /*
+ * tgisnternal is set to true for inherited triggers of partitions in
+ * the servers between v11 and v13, though still must be shown to the
+ * user. So we use another property that is true for such inherited
+ * triggers to avoid them being hidden, which is their dependendence
+ * on another trigger.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 140000)
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"
" AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9ef7f6d768..8542705c5f 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -160,6 +160,10 @@ extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *que
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5d124cf96f..5c0e7c2b79 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3311,7 +3311,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
Pushed 0001.
I had to adjust the query used in pg_dump; you changed the attribute
name in the query used for pg15, but left unchanged the one for older
branches, so pg_dump failed for all branches other than 15. Also,
psql's describe.c required a small tweak to a version number test.
https://github.com/alvherre/postgres/commit/3451612e0fa082d3ec953551c6d25432bd725502
Thanks! What was 0002 is attached, to keep cfbot happy. It's identical
to your v11-0002.
I have pushed it thinking that we would not backpatch any of this fix.
However, after running the tests and realizing that I didn't need an
initdb for either patch, I wonder if maybe the whole series *is*
backpatchable.
There is one possible problem, which is that psql and pg_dump would need
testing to verify that they work decently (i.e. no crash, no
misbehavior) with partitioned tables created with the original code.
But there are few ABI changes, maybe we can cope and get all branches
fixes instead of just 15.
What do you think?
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"Los dioses no protegen a los insensatos. Éstos reciben protección de
otros insensatos mejor dotados" (Luis Wu, Mundo Anillo)
Attachments:
v12-0001-Enforce-foreign-key-correctly-during-cross-parti.patchtext/x-diff; charset=utf-8Download
From d1b067ad541f80191763e329577e0d3f62d00d82 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH v12] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows, although it should not,
because the referenced row is simply being moved into another
partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 322 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 187 ++++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 4 +
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 135 ++++++++-
11 files changed, 840 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 452b743f21..1ed6dd1b38 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,7 +94,11 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2458,7 +2462,9 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2547,7 +2553,9 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2672,7 +2680,9 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2769,7 +2779,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2793,7 +2804,9 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2914,7 +2927,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3052,8 +3067,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
+ * destination partitions, respectively, of a cross-partition update of the
+ * root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey().
+ */
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -3073,12 +3100,15 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
+ src_partinfo != NULL ?
+ src_partinfo :
+ relinfo);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ src_partinfo != NULL ? src_partinfo : relinfo,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,7 +3118,9 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3214,7 +3246,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3531,7 +3565,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
@@ -3539,7 +3573,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_FDW_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0xC8000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -3560,8 +3595,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to
+ * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
+ ItemPointerData ate_ctid2; /* new updated tuple */
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3576,11 +3627,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3815,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4086,8 +4141,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4101,6 +4164,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4108,6 +4173,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4188,12 +4255,36 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4203,16 +4294,42 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4441,13 +4558,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4472,12 +4592,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5672,16 +5813,38 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
+ Relation rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
+ bool maybe_crosspart_update =
+ (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)));
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
@@ -5788,6 +5951,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partitioned update. In that
+ * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5848,6 +6024,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5874,11 +6060,43 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
+ AFTER_TRIGGER_2CTID) :
+ AFTER_TRIGGER_1CTID;
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the loop
+ * below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5908,12 +6126,28 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
* tell by inspection that the FK constraint will still pass.
+ * There are also some cases during cross-partition updates of a
+ * partitioned table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the update
+ * event that will be fired on the root (partitioned)
+ * target table will be used to perform the necessary
+ * foreign key enforcement action.
+ */
+ if (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5924,8 +6158,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5934,7 +6179,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a partitioned
+ * table, because the same row trigger must be present in
+ * the leaf partition(s) that are affected as part of this
+ * update and the events fired on them are queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..40b6d924ce 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 574d7d27fd..933b365cc5 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +557,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index d328856ae5..e9d1b3fb5d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,9 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
@@ -994,6 +999,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1356,8 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1372,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1444,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1569,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1584,124 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's trigger
+ * to check that a cross-partition update hasn't broken any foreign keys
+ * pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
+ sourcePartInfo, destPartInfo,
+ tupleid, NULL,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1874,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1891,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2102,10 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2722,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 8ebb2a50a1..58f0115c01 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 489c93de92..cbbf7449da 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,7 +231,10 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index cd57a704ad..4abab5e4d0 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4ff98f4040..904f1b3e55 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -525,6 +525,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.30.2
Hi Alvaro,
On Thu, Jan 6, 2022 at 7:27 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Pushed 0001.
Thank you.
I had to adjust the query used in pg_dump; you changed the attribute
name in the query used for pg15, but left unchanged the one for older
branches, so pg_dump failed for all branches other than 15.
Oops, should've tested that.
Also,
psql's describe.c required a small tweak to a version number test.
Ah, yes -- 15, not 14 anymore.
https://github.com/alvherre/postgres/commit/3451612e0fa082d3ec953551c6d25432bd725502
Thanks! What was 0002 is attached, to keep cfbot happy. It's identical
to your v11-0002.I have pushed it thinking that we would not backpatch any of this fix.
However, after running the tests and realizing that I didn't need an
initdb for either patch, I wonder if maybe the whole series *is*
backpatchable.
Interesting thought.
We do lack help from trigger.c in the v12 branch in that there's no
Trigger.tgisclone, which is used in a couple of places in the fix. I
haven't checked how big of a deal it would be to back-port
Trigger.tgisclone to v12, but maybe that's doable.
There is one possible problem, which is that psql and pg_dump would need
testing to verify that they work decently (i.e. no crash, no
misbehavior) with partitioned tables created with the original code.
I suppose you mean checking if the psql and pg_dump after applying
*0001* work sanely with partitioned tables defined without 0001?
Will test that.
But there are few ABI changes, maybe we can cope and get all branches
fixes instead of just 15.What do you think?
Yeah, as long as triggers are configured as required by the fix, and
that would be ensured if we're able to back-patch 0001 down to v12, I
suppose it would be nice to get this fixed down to v12.
--
Amit Langote
EDB: http://www.enterprisedb.com
On 2022-Jan-06, Amit Langote wrote:
On Thu, Jan 6, 2022 at 7:27 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
I have pushed it thinking that we would not backpatch any of this fix.
However, after running the tests and realizing that I didn't need an
initdb for either patch, I wonder if maybe the whole series *is*
backpatchable.We do lack help from trigger.c in the v12 branch in that there's no
Trigger.tgisclone, which is used in a couple of places in the fix. I
haven't checked how big of a deal it would be to back-port
Trigger.tgisclone to v12, but maybe that's doable.
Yeah, I realized afterwards that we added tgparentid in 13 only
(b9b408c48), so we should only backpatch to that.
There is one possible problem, which is that psql and pg_dump would need
testing to verify that they work decently (i.e. no crash, no
misbehavior) with partitioned tables created with the original code.I suppose you mean checking if the psql and pg_dump after applying
*0001* work sanely with partitioned tables defined without 0001?
Yes.
Will test that.
I looked at the backpatch at the last minute yesterday. The tablecmds.c
conflict is easy to resolve, but the one in pg_dump.c is a giant
conflict zone that I didn't have time to look closely :-(
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
On Thu, Jan 6, 2022 at 9:36 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Jan-06, Amit Langote wrote:
On Thu, Jan 6, 2022 at 7:27 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
I have pushed it thinking that we would not backpatch any of this fix.
However, after running the tests and realizing that I didn't need an
initdb for either patch, I wonder if maybe the whole series *is*
backpatchable.We do lack help from trigger.c in the v12 branch in that there's no
Trigger.tgisclone, which is used in a couple of places in the fix. I
haven't checked how big of a deal it would be to back-port
Trigger.tgisclone to v12, but maybe that's doable.Yeah, I realized afterwards that we added tgparentid in 13 only
(b9b408c48), so we should only backpatch to that.There is one possible problem, which is that psql and pg_dump would need
testing to verify that they work decently (i.e. no crash, no
misbehavior) with partitioned tables created with the original code.I suppose you mean checking if the psql and pg_dump after applying
*0001* work sanely with partitioned tables defined without 0001?Yes.
Will test that.
I looked at the backpatch at the last minute yesterday. The tablecmds.c
conflict is easy to resolve, but the one in pg_dump.c is a giant
conflict zone that I didn't have time to look closely :-(
I think I've managed to apply f4566345cf40b into v13 and v14. Patches attached.
Patched pg_dump seems to work with existing partition triggers and
psql too with some changes to the condition to decide whether to show
tgisinternal triggers when describing a partition.
As for the fix to make cross-partition updates work correctly with
foreign keys, I just realized it won't work for the users' existing
foreign keys, because the parent table's triggers that are needed for
the fix to work would not be present. Were you thinking that we'd ask
users of v13 and v14 to drop and recreate those constraints?
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
14_0001-Create-foreign-key-triggers-in-partitioned-tables-to.patchapplication/octet-stream; name=14_0001-Create-foreign-key-triggers-in-partitioned-tables-to.patchDownload
From 6687c1c6adb53c37fb8b40fb539f10ab6f4066b0 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Wed, 5 Jan 2022 19:00:13 -0300
Subject: [PATCH] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
Author: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Reviewed-by: Arne Roland <A.Roland@index.de>
Discussion: https://postgr.es/m/CA+HiwqG7LQSK+n8Bki8tWv7piHD=PnZro2y6ysU2-28JS6cfgQ@mail.gmail.com
---
src/backend/commands/tablecmds.c | 414 +++++++++++++++++++++----
src/backend/commands/trigger.c | 101 +++++-
src/bin/pg_dump/pg_dump.c | 24 +-
src/bin/pg_dump/pg_dump.h | 2 +-
src/bin/psql/describe.c | 16 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 4 +-
7 files changed, 473 insertions(+), 92 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4729a895e8..07051a5b75 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -482,12 +482,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -495,15 +497,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -9227,7 +9244,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9240,7 +9258,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9274,13 +9293,18 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -9288,6 +9312,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9384,15 +9410,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9436,7 +9460,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9479,14 +9504,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9495,19 +9527,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9536,6 +9570,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9577,7 +9620,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9656,10 +9702,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9715,6 +9765,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9744,6 +9795,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9761,6 +9820,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9827,6 +9888,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9839,11 +9910,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9866,6 +9941,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9887,6 +9963,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9916,6 +10000,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9944,6 +10030,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents to
+ * the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9962,7 +10061,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -10061,9 +10163,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -10084,16 +10190,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -10154,12 +10264,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -10192,13 +10300,136 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ {
+ Assert(*deleteTriggerOid == InvalidOid);
+ *deleteTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid))
+ elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ {
+ Assert(*insertTriggerOid == InvalidOid);
+ *insertTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid))
+ elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -11115,10 +11346,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -11158,23 +11398,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -11226,9 +11475,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -11283,25 +11535,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17596,19 +17858,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17910,6 +18163,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ Relation trigrel = NULL;
if (concurrent)
{
@@ -17928,12 +18182,16 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -17951,6 +18209,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -17965,11 +18237,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -18194,6 +18470,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because these will be detached when the foreign keys
+ * themselves are.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 40441fdb4c..7b12ee921e 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -126,8 +126,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -196,6 +198,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -734,6 +737,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -760,17 +764,16 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) &&
+ !isInternal && !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -867,7 +870,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when;
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1236,6 +1239,82 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 53d3af08fe..cba909ab7e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7974,7 +7974,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid,
i_tgconstrrelname,
i_tgenabled,
- i_tgisinternal,
+ i_tgispartition,
i_tgdeferrable,
i_tginitdeferred,
i_tgdef;
@@ -8000,18 +8000,20 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
* could result in non-forward-compatible dumps of WHEN clauses
* due to under-parenthesization.
*
- * NB: We need to see tgisinternal triggers in partitions, in case
- * the tgenabled flag has been changed from the parent.
+ * NB: We need to see partition triggers in case the tgenabled flag
+ * has been changed from the parent.
*/
appendPQExpBuffer(query,
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition\n"
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
"WHERE t.tgrelid = '%u'::pg_catalog.oid "
- "AND (NOT t.tgisinternal OR t.tgenabled != u.tgenabled)",
+ "AND ((NOT t.tgisinternal AND t.tgparentid = 0) "
+ "OR t.tgenabled != u.tgenabled)",
tbinfo->dobj.catId.oid);
}
else if (fout->remoteVersion >= 110000)
@@ -8114,7 +8116,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid = PQfnumber(res, "tgconstrrelid");
i_tgconstrrelname = PQfnumber(res, "tgconstrrelname");
i_tgenabled = PQfnumber(res, "tgenabled");
- i_tgisinternal = PQfnumber(res, "tgisinternal");
+ i_tgispartition = PQfnumber(res, "tgispartition");
i_tgdeferrable = PQfnumber(res, "tgdeferrable");
i_tginitdeferred = PQfnumber(res, "tginitdeferred");
i_tgdef = PQfnumber(res, "tgdef");
@@ -8134,7 +8136,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tginfo[j].dobj.namespace = tbinfo->dobj.namespace;
tginfo[j].tgtable = tbinfo;
tginfo[j].tgenabled = *(PQgetvalue(res, j, i_tgenabled));
- tginfo[j].tgisinternal = *(PQgetvalue(res, j, i_tgisinternal)) == 't';
+ tginfo[j].tgispartition = *(PQgetvalue(res, j, i_tgispartition)) == 't';
if (i_tgdef >= 0)
{
tginfo[j].tgdef = pg_strdup(PQgetvalue(res, j, i_tgdef));
@@ -17763,11 +17765,13 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
"pg_catalog.pg_trigger", "TRIGGER",
trigidentity->data);
- if (tginfo->tgisinternal)
+ if (tginfo->tgispartition)
{
+ Assert(tbinfo->ispartition);
+
/*
- * Triggers marked internal only appear here because their 'tgenabled'
- * flag differs from its parent's. The trigger is created already, so
+ * Partition triggers only appear here because their 'tgenabled' flag
+ * differs from its parent's. The trigger is created already, so
* remove the CREATE and replace it with an ALTER. (Clear out the
* DROP query too, so that pg_dump --create does not cause errors.)
*/
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 8b2a8b67e7..0a58674a78 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -425,7 +425,7 @@ typedef struct _triggerInfo
Oid tgconstrrelid;
char *tgconstrrelname;
char tgenabled;
- bool tgisinternal;
+ bool tgispartition;
bool tgdeferrable;
bool tginitdeferred;
char *tgdef;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 48348750ee..f57d0af289 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3266,10 +3266,20 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+
+ /*
+ * tgisinternal is set true for inherited triggers of partitions in
+ * servers betweem v11 and v14, though these must still be shown to
+ * the user. So we use another property that is true for such
+ * inherited triggers to avoid them being hidden, which is their
+ * dependendence on another trigger; must still hide the internal
+ * triggers thar originate from a constraint.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 150000)
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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
+ " OR (EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
+ " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass) \n"
+ " AND t.tgconstraint = 0))");
else if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9ef7f6d768..489c93de92 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -160,6 +160,10 @@ extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *que
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5254447cf8..ae342bae7f 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3311,7 +3311,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
13_0001-Create-foreign-key-triggers-in-partitioned-tables-to.patchapplication/octet-stream; name=13_0001-Create-foreign-key-triggers-in-partitioned-tables-to.patchDownload
From 9f25ba0a9a6e1288500b93598dc05b9066a16ed1 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Wed, 5 Jan 2022 19:00:13 -0300
Subject: [PATCH] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
Author: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Reviewed-by: Arne Roland <A.Roland@index.de>
Discussion: https://postgr.es/m/CA+HiwqG7LQSK+n8Bki8tWv7piHD=PnZro2y6ysU2-28JS6cfgQ@mail.gmail.com
---
src/backend/commands/tablecmds.c | 414 +++++++++++++++++++++----
src/backend/commands/trigger.c | 84 ++++-
src/bin/pg_dump/pg_dump.c | 24 +-
src/bin/pg_dump/pg_dump.h | 2 +-
src/bin/psql/describe.c | 16 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 2 +-
7 files changed, 463 insertions(+), 83 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b82d32ba1..0539642817 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -453,12 +453,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -466,15 +468,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -8833,7 +8850,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8846,7 +8864,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -8880,13 +8899,18 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -8894,6 +8918,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -8990,15 +9016,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9042,7 +9066,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9085,14 +9110,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9101,19 +9133,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9142,6 +9176,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9183,7 +9226,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9262,10 +9308,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9321,6 +9371,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9350,6 +9401,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9367,6 +9426,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9433,6 +9494,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9445,11 +9516,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9472,6 +9547,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9493,6 +9569,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9522,6 +9606,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9550,6 +9636,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents to
+ * the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9568,7 +9667,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9667,9 +9769,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9690,16 +9796,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -9760,12 +9870,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -9798,13 +9906,136 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ {
+ Assert(*deleteTriggerOid == InvalidOid);
+ *deleteTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid))
+ elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ {
+ Assert(*insertTriggerOid == InvalidOid);
+ *insertTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid))
+ elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10721,10 +10952,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10763,23 +11003,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->constrrel = NULL;
fk_trigger->args = NIL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10830,9 +11079,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
}
fk_trigger->args = NIL;
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10886,25 +11138,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
}
fk_trigger->args = NIL;
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17086,19 +17348,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17217,6 +17470,7 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
List *indexes;
List *fks;
ListCell *cell;
+ Relation trigrel = NULL;
/*
* We must lock the default partition, because detaching this partition
@@ -17309,12 +17563,16 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -17332,6 +17590,20 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -17346,11 +17618,15 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -17441,6 +17717,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because these will be detached when the foreign keys
+ * themselves are.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 788b92c7b8..c9d7702318 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -125,8 +125,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -809,7 +811,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when;
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1161,6 +1163,82 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 147a860b9b..796901450e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7844,7 +7844,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid,
i_tgconstrrelname,
i_tgenabled,
- i_tgisinternal,
+ i_tgispartition,
i_tgdeferrable,
i_tginitdeferred,
i_tgdef;
@@ -7870,18 +7870,20 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
* could result in non-forward-compatible dumps of WHEN clauses
* due to under-parenthesization.
*
- * NB: We need to see tgisinternal triggers in partitions, in case
- * the tgenabled flag has been changed from the parent.
+ * NB: We need to see partition triggers in case the tgenabled flag
+ * has been changed from the parent.
*/
appendPQExpBuffer(query,
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition\n"
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
"WHERE t.tgrelid = '%u'::pg_catalog.oid "
- "AND (NOT t.tgisinternal OR t.tgenabled != u.tgenabled)",
+ "AND ((NOT t.tgisinternal AND t.tgparentid = 0) "
+ "OR t.tgenabled != u.tgenabled) ",
tbinfo->dobj.catId.oid);
}
else if (fout->remoteVersion >= 110000)
@@ -7984,7 +7986,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid = PQfnumber(res, "tgconstrrelid");
i_tgconstrrelname = PQfnumber(res, "tgconstrrelname");
i_tgenabled = PQfnumber(res, "tgenabled");
- i_tgisinternal = PQfnumber(res, "tgisinternal");
+ i_tgispartition = PQfnumber(res, "tgispartition");
i_tgdeferrable = PQfnumber(res, "tgdeferrable");
i_tginitdeferred = PQfnumber(res, "tginitdeferred");
i_tgdef = PQfnumber(res, "tgdef");
@@ -8004,7 +8006,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tginfo[j].dobj.namespace = tbinfo->dobj.namespace;
tginfo[j].tgtable = tbinfo;
tginfo[j].tgenabled = *(PQgetvalue(res, j, i_tgenabled));
- tginfo[j].tgisinternal = *(PQgetvalue(res, j, i_tgisinternal)) == 't';
+ tginfo[j].tgispartition = *(PQgetvalue(res, j, i_tgispartition)) == 't';
if (i_tgdef >= 0)
{
tginfo[j].tgdef = pg_strdup(PQgetvalue(res, j, i_tgdef));
@@ -17753,11 +17755,13 @@ dumpTrigger(Archive *fout, TriggerInfo *tginfo)
"pg_catalog.pg_trigger", "TRIGGER",
trigidentity->data);
- if (tginfo->tgisinternal)
+ if (tginfo->tgispartition)
{
+ Assert(tbinfo->ispartition);
+
/*
- * Triggers marked internal only appear here because their 'tgenabled'
- * flag differs from its parent's. The trigger is created already, so
+ * Partition triggers only appear here because their 'tgenabled' flag
+ * differs from its parent's. The trigger is created already, so
* remove the CREATE and replace it with an ALTER. (Clear out the
* DROP query too, so that pg_dump --create does not cause errors.)
*/
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7d41064b8a..4db409a328 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -415,7 +415,7 @@ typedef struct _triggerInfo
Oid tgconstrrelid;
char *tgconstrrelname;
char tgenabled;
- bool tgisinternal;
+ bool tgispartition;
bool tgdeferrable;
bool tginitdeferred;
char *tgdef;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 384db60878..c16cc1938e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2969,10 +2969,20 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+
+ /*
+ * tgisinternal is set true for inherited triggers of partitions in
+ * servers betweem v11 and v14, though these must still be shown to
+ * the user. So we use another property that is true for such
+ * inherited triggers to avoid them being hidden, which is their
+ * dependendence on another trigger; must still hide the internal
+ * triggers thar originate from a constraint.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 150000)
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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
+ " OR (EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
+ " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass) \n"
+ " AND t.tgconstraint = 0))");
else if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 40b8154876..1873ce14da 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -168,6 +168,10 @@ extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *que
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 207e0d3779..397f7763a0 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
--
2.24.1
On 2022-Jan-11, Amit Langote wrote:
As for the fix to make cross-partition updates work correctly with
foreign keys, I just realized it won't work for the users' existing
foreign keys, because the parent table's triggers that are needed for
the fix to work would not be present. Were you thinking that we'd ask
users of v13 and v14 to drop and recreate those constraints?
Yeah, more or less. Also, any tables created from 13.6 onwards.
I was mainly thinking that we'll still have people creating new clusters
using pg13 for half a decade.
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
"Right now the sectors on the hard disk run clockwise, but I heard a rumor that
you can squeeze 0.2% more throughput by running them counterclockwise.
It's worth the effort. Recommended." (Gerry Pourwelle)
Hi,
On Tue, Jan 11, 2022 at 05:08:59PM +0900, Amit Langote wrote:
I think I've managed to apply f4566345cf40b into v13 and v14. Patches attached.
FTR this doesn't play well with the cfbot unfortunately as it tries to apply
both patches, and obviously on the wrong branches anyway.
It means that the previous-0002-now-0001 patch that �lvaro previously sent
(/messages/by-id/202201052227.bc4yvvy6lqpb@alvherre.pgsql)
is not tested anymore, and IIUC it's not pushed yet so it's not ideal.
There's now an official documentation on how to send patches that should be
ignored by the cfbot [1]https://wiki.postgresql.org/wiki/Cfbot#Which_attachments_are_considered_to_be_patches.3F, so sending backpatch versions with a .txt extension
could be useful. Just in case I'm attaching the pending patch to this mail to
make the cfbot happy again.
[1]: https://wiki.postgresql.org/wiki/Cfbot#Which_attachments_are_considered_to_be_patches.3F
Attachments:
v12-0001-Enforce-foreign-key-correctly-during-cross-parti.patchtext/plain; charset=us-asciiDownload
From d1b067ad541f80191763e329577e0d3f62d00d82 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH v12] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows, although it should not,
because the referenced row is simply being moved into another
partition.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the "root" target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the latter.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, it sounds rare to have distinct foreign keys
pointing into sub-partitioned partitions, but not into the root
table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 322 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 187 ++++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 4 +
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 135 ++++++++-
11 files changed, 840 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 452b743f21..1ed6dd1b38 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,7 +94,11 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2458,7 +2462,9 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2547,7 +2553,9 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2672,7 +2680,9 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2769,7 +2779,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2793,7 +2804,9 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2914,7 +2927,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3052,8 +3067,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
+ * destination partitions, respectively, of a cross-partition update of the
+ * root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey().
+ */
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -3073,12 +3100,15 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
+ src_partinfo != NULL ?
+ src_partinfo :
+ relinfo);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ src_partinfo != NULL ? src_partinfo : relinfo,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,7 +3118,9 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3214,7 +3246,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3531,7 +3565,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
@@ -3539,7 +3573,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_FDW_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0xC8000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -3560,8 +3595,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to
+ * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
+ ItemPointerData ate_ctid2; /* new updated tuple */
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3576,11 +3627,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3815,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4086,8 +4141,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4101,6 +4164,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4108,6 +4173,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4188,12 +4255,36 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4203,16 +4294,42 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4441,13 +4558,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4472,12 +4592,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5672,16 +5813,38 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
+ Relation rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
+ bool maybe_crosspart_update =
+ (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)));
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
@@ -5788,6 +5951,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partitioned update. In that
+ * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5848,6 +6024,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5874,11 +6060,43 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
+ AFTER_TRIGGER_2CTID) :
+ AFTER_TRIGGER_1CTID;
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the loop
+ * below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5908,12 +6126,28 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
* tell by inspection that the FK constraint will still pass.
+ * There are also some cases during cross-partition updates of a
+ * partitioned table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the update
+ * event that will be fired on the root (partitioned)
+ * target table will be used to perform the necessary
+ * foreign key enforcement action.
+ */
+ if (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5924,8 +6158,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5934,7 +6179,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a partitioned
+ * table, because the same row trigger must be present in
+ * the leaf partition(s) that are affected as part of this
+ * update and the events fired on them are queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..40b6d924ce 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 574d7d27fd..933b365cc5 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +557,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index d328856ae5..e9d1b3fb5d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,9 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
@@ -994,6 +999,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1356,8 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1372,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1444,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1569,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1584,124 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's trigger
+ * to check that a cross-partition update hasn't broken any foreign keys
+ * pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
+ sourcePartInfo, destPartInfo,
+ tupleid, NULL,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1874,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1891,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2102,10 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2722,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 8ebb2a50a1..58f0115c01 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 489c93de92..cbbf7449da 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,7 +231,10 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index cd57a704ad..4abab5e4d0 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4ff98f4040..904f1b3e55 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -525,6 +525,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.30.2
On Thu, Jan 13, 2022 at 12:19 PM Julien Rouhaud <rjuju123@gmail.com> wrote:
On Tue, Jan 11, 2022 at 05:08:59PM +0900, Amit Langote wrote:
I think I've managed to apply f4566345cf40b into v13 and v14. Patches attached.
FTR this doesn't play well with the cfbot unfortunately as it tries to apply
both patches, and obviously on the wrong branches anyway.
Oops, that's right. Thanks for the heads up.
It means that the previous-0002-now-0001 patch that Álvaro previously sent
(/messages/by-id/202201052227.bc4yvvy6lqpb@alvherre.pgsql)
is not tested anymore, and IIUC it's not pushed yet so it's not ideal.
Agreed.
There's now an official documentation on how to send patches that should be
ignored by the cfbot [1], so sending backpatch versions with a .txt extension
could be useful. Just in case I'm attaching the pending patch to this mail to
make the cfbot happy again.
Thanks and sorry I wasn't aware of the rule about sending back patch versions.
--
Amit Langote
EDB: http://www.enterprisedb.com
On Tue, Jan 11, 2022 at 8:23 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Jan-11, Amit Langote wrote:
As for the fix to make cross-partition updates work correctly with
foreign keys, I just realized it won't work for the users' existing
foreign keys, because the parent table's triggers that are needed for
the fix to work would not be present. Were you thinking that we'd ask
users of v13 and v14 to drop and recreate those constraints?Yeah, more or less. Also, any tables created from 13.6 onwards.
I was mainly thinking that we'll still have people creating new clusters
using pg13 for half a decade.
Okay, I created versions of the patch series for branches 13 and 14
(.txt files). The one for HEAD is also re-attached.
Note that the fix involves adding fields to ResultRelInfo -- v13 needs
2 additional, while v14 and HEAD need 1. That combined with needing
new catalog entries for parent FK triggers, back-patching this does
make me a bit uncomfortable. Another thing to consider is that we
haven't seen many reports of the problem (UPDATEs of partitioned PK
tables causing DELETEs in referencing tables), even though it can be
possibly very surprising to those who do run into it.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
14_v12-0002-Enforce-foreign-key-correctly-during-cross-partition.patch.txttext/plain; charset=US-ASCII; name=14_v12-0002-Enforce-foreign-key-correctly-during-cross-partition.patch.txtDownload
From 36fa984f5561d671c681167e35656cb0806ff852 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 322 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 187 ++++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 4 +
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 135 ++++++++-
11 files changed, 840 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7b12ee921e..8d50bcb83c 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -88,7 +88,11 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2325,7 +2329,9 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2414,7 +2420,9 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2539,7 +2547,9 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2636,7 +2646,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2660,7 +2671,9 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2781,7 +2794,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -2919,8 +2934,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
+ * destination partitions, respectively, of a cross-partition update of the
+ * root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey().
+ */
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2940,12 +2967,15 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
+ src_partinfo != NULL ?
+ src_partinfo :
+ relinfo);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ src_partinfo != NULL ? src_partinfo : relinfo,
tupleid,
LockTupleExclusive,
oldslot,
@@ -2955,7 +2985,9 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3081,7 +3113,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3398,7 +3432,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
@@ -3406,7 +3440,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_FDW_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0xC8000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -3427,8 +3462,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to
+ * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
+ ItemPointerData ate_ctid2; /* new updated tuple */
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3443,11 +3494,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3629,6 +3682,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -3953,8 +4008,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -3968,6 +4031,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -3975,6 +4040,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4055,12 +4122,36 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4070,16 +4161,42 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4308,13 +4425,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4339,12 +4459,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5539,16 +5680,38 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
+ Relation rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
+ bool maybe_crosspart_update =
+ (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)));
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
@@ -5655,6 +5818,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partitioned update. In that
+ * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5715,6 +5891,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5741,11 +5927,43 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
+ AFTER_TRIGGER_2CTID) :
+ AFTER_TRIGGER_1CTID;
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the loop
+ * below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5775,12 +5993,28 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
* tell by inspection that the FK constraint will still pass.
+ * There are also some cases during cross-partition updates of a
+ * partitioned table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the update
+ * event that will be fired on the root (partitioned)
+ * target table will be used to perform the necessary
+ * foreign key enforcement action.
+ */
+ if (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5791,8 +6025,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5801,7 +6046,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a partitioned
+ * table, because the same row trigger must be present in
+ * the leaf partition(s) that are affected as part of this
+ * update and the events fired on them are queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b3ce4bae53..40b6d924ce 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 1e285e0349..9ca79a8aed 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +557,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index d328856ae5..e9d1b3fb5d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,9 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
@@ -994,6 +999,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1356,8 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1372,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1444,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1569,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1584,124 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's trigger
+ * to check that a cross-partition update hasn't broken any foreign keys
+ * pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
+ sourcePartInfo, destPartInfo,
+ tupleid, NULL,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1874,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1891,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2102,10 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2722,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 96269fc2ad..38b3b9e53f 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1208,6 +1208,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 489c93de92..cbbf7449da 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,7 +231,10 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 3dc03c913e..acf2b9b240 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 3dfac5bd5f..30785e4b55 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -524,6 +524,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index bf794dce9d..6a0f499fcb 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2485,7 +2485,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2554,15 +2554,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index de417b62b6..6c69a69c7a 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1820,12 +1820,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
14_v12-0001-Create-foreign-key-triggers-in-partitioned-tables-to.patch.txttext/plain; charset=US-ASCII; name=14_v12-0001-Create-foreign-key-triggers-in-partitioned-tables-to.patch.txtDownload
From e02f2a1e4eb0975e984af96af09ca50cbae81e26 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Wed, 5 Jan 2022 19:00:13 -0300
Subject: [PATCH 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
Author: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Reviewed-by: Arne Roland <A.Roland@index.de>
Discussion: https://postgr.es/m/CA+HiwqG7LQSK+n8Bki8tWv7piHD=PnZro2y6ysU2-28JS6cfgQ@mail.gmail.com
---
src/backend/commands/tablecmds.c | 414 +++++++++++++++++++++----
src/backend/commands/trigger.c | 101 +++++-
src/bin/pg_dump/pg_dump.c | 24 +-
src/bin/pg_dump/pg_dump.h | 2 +-
src/bin/psql/describe.c | 16 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 4 +-
7 files changed, 473 insertions(+), 92 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4729a895e8..07051a5b75 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -482,12 +482,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -495,15 +497,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -9227,7 +9244,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -9240,7 +9258,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -9274,13 +9293,18 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -9288,6 +9312,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -9384,15 +9410,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9436,7 +9460,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9479,14 +9504,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9495,19 +9527,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9536,6 +9570,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9577,7 +9620,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9656,10 +9702,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9715,6 +9765,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9744,6 +9795,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9761,6 +9820,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9827,6 +9888,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9839,11 +9910,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9866,6 +9941,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9887,6 +9963,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9916,6 +10000,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9944,6 +10030,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents to
+ * the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9962,7 +10061,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -10061,9 +10163,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -10084,16 +10190,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -10154,12 +10264,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -10192,13 +10300,136 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ {
+ Assert(*deleteTriggerOid == InvalidOid);
+ *deleteTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid))
+ elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ {
+ Assert(*insertTriggerOid == InvalidOid);
+ *insertTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid))
+ elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -11115,10 +11346,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -11158,23 +11398,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->initdeferred = fkconstraint->initdeferred;
fk_trigger->constrrel = NULL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -11226,9 +11475,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -11283,25 +11535,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
break;
}
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17596,19 +17858,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17910,6 +18163,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
new_repl[Natts_pg_class];
HeapTuple tuple,
newtuple;
+ Relation trigrel = NULL;
if (concurrent)
{
@@ -17928,12 +18182,16 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -17951,6 +18209,20 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -17965,11 +18237,15 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -18194,6 +18470,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because these will be detached when the foreign keys
+ * themselves are.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 40441fdb4c..7b12ee921e 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -126,8 +126,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -196,6 +198,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
bool trigger_exists = false;
Oid existing_constraint_oid = InvalidOid;
bool existing_isInternal = false;
+ bool existing_isClone = false;
if (OidIsValid(relOid))
rel = table_open(relOid, ShareRowExclusiveLock);
@@ -734,6 +737,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
trigoid = oldtrigger->oid;
existing_constraint_oid = oldtrigger->tgconstraint;
existing_isInternal = oldtrigger->tgisinternal;
+ existing_isClone = OidIsValid(oldtrigger->tgparentid);
trigger_exists = true;
/* copy the tuple to use in CatalogTupleUpdate() */
tuple = heap_copytuple(tuple);
@@ -760,17 +764,16 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
stmt->trigname, RelationGetRelationName(rel))));
/*
- * An internal trigger cannot be replaced by a user-defined trigger.
- * However, skip this test when in_partition, because then we're
- * recursing from a partitioned table and the check was made at the
- * parent level. Child triggers will always be marked "internal" (so
- * this test does protect us from the user trying to replace a child
- * trigger directly).
+ * An internal trigger or a child trigger (isClone) cannot be replaced
+ * by a user-defined trigger. However, skip this test when
+ * in_partition, because then we're recursing from a partitioned table
+ * and the check was made at the parent level.
*/
- if (existing_isInternal && !isInternal && !in_partition)
+ if ((existing_isInternal || existing_isClone) &&
+ !isInternal && !in_partition)
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" is an internal trigger",
+ errmsg("trigger \"%s\" for relation \"%s\" is an internal or a child trigger",
stmt->trigname, RelationGetRelationName(rel))));
/*
@@ -867,7 +870,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when;
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1236,6 +1239,82 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 53d3af08fe..cba909ab7e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7974,7 +7974,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid,
i_tgconstrrelname,
i_tgenabled,
- i_tgisinternal,
+ i_tgispartition,
i_tgdeferrable,
i_tginitdeferred,
i_tgdef;
@@ -8000,18 +8000,20 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
* could result in non-forward-compatible dumps of WHEN clauses
* due to under-parenthesization.
*
- * NB: We need to see tgisinternal triggers in partitions, in case
- * the tgenabled flag has been changed from the parent.
+ * NB: We need to see partition triggers in case the tgenabled flag
+ * has been changed from the parent.
*/
appendPQExpBuffer(query,
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition\n"
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
"WHERE t.tgrelid = '%u'::pg_catalog.oid "
- "AND (NOT t.tgisinternal OR t.tgenabled != u.tgenabled)",
+ "AND ((NOT t.tgisinternal AND t.tgparentid = 0) "
+ "OR t.tgenabled != u.tgenabled)",
tbinfo->dobj.catId.oid);
}
else if (fout->remoteVersion >= 110000)
@@ -8114,7 +8116,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid = PQfnumber(res, "tgconstrrelid");
i_tgconstrrelname = PQfnumber(res, "tgconstrrelname");
i_tgenabled = PQfnumber(res, "tgenabled");
- i_tgisinternal = PQfnumber(res, "tgisinternal");
+ i_tgispartition = PQfnumber(res, "tgispartition");
i_tgdeferrable = PQfnumber(res, "tgdeferrable");
i_tginitdeferred = PQfnumber(res, "tginitdeferred");
i_tgdef = PQfnumber(res, "tgdef");
@@ -8134,7 +8136,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tginfo[j].dobj.namespace = tbinfo->dobj.namespace;
tginfo[j].tgtable = tbinfo;
tginfo[j].tgenabled = *(PQgetvalue(res, j, i_tgenabled));
- tginfo[j].tgisinternal = *(PQgetvalue(res, j, i_tgisinternal)) == 't';
+ tginfo[j].tgispartition = *(PQgetvalue(res, j, i_tgispartition)) == 't';
if (i_tgdef >= 0)
{
tginfo[j].tgdef = pg_strdup(PQgetvalue(res, j, i_tgdef));
@@ -17763,11 +17765,13 @@ dumpTrigger(Archive *fout, const TriggerInfo *tginfo)
"pg_catalog.pg_trigger", "TRIGGER",
trigidentity->data);
- if (tginfo->tgisinternal)
+ if (tginfo->tgispartition)
{
+ Assert(tbinfo->ispartition);
+
/*
- * Triggers marked internal only appear here because their 'tgenabled'
- * flag differs from its parent's. The trigger is created already, so
+ * Partition triggers only appear here because their 'tgenabled' flag
+ * differs from its parent's. The trigger is created already, so
* remove the CREATE and replace it with an ALTER. (Clear out the
* DROP query too, so that pg_dump --create does not cause errors.)
*/
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 8b2a8b67e7..0a58674a78 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -425,7 +425,7 @@ typedef struct _triggerInfo
Oid tgconstrrelid;
char *tgconstrrelname;
char tgenabled;
- bool tgisinternal;
+ bool tgispartition;
bool tgdeferrable;
bool tginitdeferred;
char *tgdef;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 48348750ee..f57d0af289 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3266,10 +3266,20 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+
+ /*
+ * tgisinternal is set true for inherited triggers of partitions in
+ * servers betweem v11 and v14, though these must still be shown to
+ * the user. So we use another property that is true for such
+ * inherited triggers to avoid them being hidden, which is their
+ * dependendence on another trigger; must still hide the internal
+ * triggers thar originate from a constraint.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 150000)
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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
+ " OR (EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
+ " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass) \n"
+ " AND t.tgconstraint = 0))");
else if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9ef7f6d768..489c93de92 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -160,6 +160,10 @@ extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *que
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5254447cf8..ae342bae7f 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
@@ -3311,7 +3311,7 @@ NOTICE: hello from funcA
create or replace trigger my_trig
after insert on parted_trig_1
for each row execute procedure funcB(); -- should fail
-ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal trigger
+ERROR: trigger "my_trig" for relation "parted_trig_1" is an internal or a child trigger
insert into parted_trig (a) values (50);
NOTICE: hello from funcA
drop trigger my_trig on parted_trig;
--
2.24.1
v12-0001-Enforce-foreign-key-correctly-during-cross-parti.patchapplication/octet-stream; name=v12-0001-Enforce-foreign-key-correctly-during-cross-parti.patchDownload
From 07794959879fc741eac5fe092178e587b7cd4b3d Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH v12] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 322 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 187 ++++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 4 +
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 135 ++++++++-
11 files changed, 840 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 1a9c1ac290..0544efab56 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,7 +94,11 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2458,7 +2462,9 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2547,7 +2553,9 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2672,7 +2680,9 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2769,7 +2779,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2793,7 +2804,9 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2914,7 +2927,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3052,8 +3067,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
+ * destination partitions, respectively, of a cross-partition update of the
+ * root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey().
+ */
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -3073,12 +3100,15 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
+ src_partinfo != NULL ?
+ src_partinfo :
+ relinfo);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ src_partinfo != NULL ? src_partinfo : relinfo,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,7 +3118,9 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3214,7 +3246,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3531,7 +3565,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
@@ -3539,7 +3573,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_FDW_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0xC8000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -3560,8 +3595,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to
+ * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
+ ItemPointerData ate_ctid2; /* new updated tuple */
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3576,11 +3627,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3815,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4086,8 +4141,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4101,6 +4164,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4108,6 +4173,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4188,12 +4255,36 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4203,16 +4294,42 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4441,13 +4558,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4472,12 +4592,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5672,16 +5813,38 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
+ Relation rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
+ bool maybe_crosspart_update =
+ (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)));
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
@@ -5788,6 +5951,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partitioned update. In that
+ * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5848,6 +6024,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5874,11 +6060,43 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
+ AFTER_TRIGGER_2CTID) :
+ AFTER_TRIGGER_1CTID;
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the loop
+ * below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5908,12 +6126,28 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
* tell by inspection that the FK constraint will still pass.
+ * There are also some cases during cross-partition updates of a
+ * partitioned table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the update
+ * event that will be fired on the root (partitioned)
+ * target table will be used to perform the necessary
+ * foreign key enforcement action.
+ */
+ if (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5924,8 +6158,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5934,7 +6179,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a partitioned
+ * table, because the same row trigger must be present in
+ * the leaf partition(s) that are affected as part of this
+ * update and the events fired on them are queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 549d9eb696..b6a2d4f708 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 313c87398b..e2a338ba33 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +557,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 5ec699a9bd..7d32630030 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,9 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
@@ -994,6 +999,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1356,8 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1372,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1444,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1569,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1584,124 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's trigger
+ * to check that a cross-partition update hasn't broken any foreign keys
+ * pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
+ sourcePartInfo, destPartInfo,
+ tupleid, NULL,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1874,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1891,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2102,10 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2722,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index c95cd32402..01d4c22cfc 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index e1271420e5..1ba3a54499 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,7 +231,10 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 344399f6a8..dcff6aeca5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4ea8735dd8..6c7eef1e54 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -529,6 +529,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
13_v12-0001-Create-foreign-key-triggers-in-partitioned-tables-to.patch.txttext/plain; charset=US-ASCII; name=13_v12-0001-Create-foreign-key-triggers-in-partitioned-tables-to.patch.txtDownload
From bf6fe296e61de92729a445ba68220a26b8282d32 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Wed, 5 Jan 2022 19:00:13 -0300
Subject: [PATCH 1/2] Create foreign key triggers in partitioned tables too
While user-defined triggers defined on a partitioned table have
a catalog definition for both it and its partitions, internal
triggers used by foreign keys defined on partitioned tables only
have a catalog definition for its partitions. This commit fixes
that so that partitioned tables get the foreign key triggers too,
just like user-defined triggers. Moreover, like user-defined
triggers, partitions' internal triggers will now also have their
tgparentid set appropriately. This is to allow subsequent commit(s)
to make the foreign key related events to be fired in some cases
using the parent table triggers instead of those of partitions'.
This also changes what tgisinternal means in some cases. Currently,
it means either that the trigger is an internal implementation object
of a foreign key constraint, or a "child" trigger on a partition
cloned from the trigger on the parent. This commit changes it to
only mean the former to avoid confusion. As for the latter, it can
be told by tgparentid being nonzero, which is now true both for user-
defined and foreign key's internal triggers.
Author: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Reviewed-by: Arne Roland <A.Roland@index.de>
Discussion: https://postgr.es/m/CA+HiwqG7LQSK+n8Bki8tWv7piHD=PnZro2y6ysU2-28JS6cfgQ@mail.gmail.com
---
src/backend/commands/tablecmds.c | 414 +++++++++++++++++++++----
src/backend/commands/trigger.c | 84 ++++-
src/bin/pg_dump/pg_dump.c | 24 +-
src/bin/pg_dump/pg_dump.h | 2 +-
src/bin/psql/describe.c | 16 +-
src/include/commands/trigger.h | 4 +
src/test/regress/expected/triggers.out | 2 +-
7 files changed, 463 insertions(+), 83 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b82d32ba1..0539642817 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -453,12 +453,14 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok);
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger);
static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint,
Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode);
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger);
static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel,
Relation partitionRel);
static void CloneFkReferenced(Relation parentRel, Relation partitionRel);
@@ -466,15 +468,30 @@ static void CloneFkReferencing(List **wqueue, Relation parentRel,
Relation partRel);
static void createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid);
static void createForeignKeyActionTriggers(Relation rel, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid);
+ Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid);
static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
Oid partRelid,
Oid parentConstrOid, int numfks,
AttrNumber *mapped_conkey, AttrNumber *confkey,
- Oid *conpfeqop);
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel);
+static void GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid);
+static void GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -8833,7 +8850,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
pfeqoperators,
ppeqoperators,
ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ InvalidOid, InvalidOid);
/* Now handle the referencing side. */
addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel,
@@ -8846,7 +8864,8 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ InvalidOid, InvalidOid);
/*
* Done. Close pk table, but keep lock until we've committed.
@@ -8880,13 +8899,18 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* pf/pp/ffeqoperators are OID array of operators between columns.
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
+ * parentDelTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent action triggers for DELETE and
+ * UPDATE respectively.
*/
static ObjectAddress
addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks,
int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators,
- Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok)
+ Oid *ppeqoperators, Oid *ffeqoperators,
+ bool old_check_ok,
+ Oid parentDelTrigger, Oid parentUpdTrigger)
{
ObjectAddress address;
Oid constrOid;
@@ -8894,6 +8918,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
bool conislocal;
int coninhcount;
bool connoinherit;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
/*
* Verify relkind for each referenced partition. At the top level, this
@@ -8990,15 +9016,13 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
CommandCounterIncrement();
/*
- * If the referenced table is a plain relation, create the action triggers
- * that enforce the constraint.
+ * Create the action triggers that enforce the constraint.
*/
- if (pkrel->rd_rel->relkind == RELKIND_RELATION)
- {
- createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
- fkconstraint,
- constrOid, indexOid);
- }
+ createForeignKeyActionTriggers(rel, RelationGetRelid(pkrel),
+ fkconstraint,
+ constrOid, indexOid,
+ parentDelTrigger, parentUpdTrigger,
+ &deleteTriggerOid, &updateTriggerOid);
/*
* If the referenced table is partitioned, recurse on ourselves to handle
@@ -9042,7 +9066,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
partIndexId, constrOid, numfks,
mapped_pkattnum, fkattnum,
pfeqoperators, ppeqoperators, ffeqoperators,
- old_check_ok);
+ old_check_ok,
+ deleteTriggerOid, updateTriggerOid);
/* Done -- clean up (but keep the lock) */
table_close(partRel, NoLock);
@@ -9085,14 +9110,21 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
* old_check_ok signals that this constraint replaces an existing one that
* was already validated (thus this one doesn't need validation).
* lockmode is the lockmode to acquire on partitions when recursing.
+ * parentInsTrigger and parentUpdTrigger, when being recursively called on
+ * a partition, are the OIDs of the parent check triggers for INSERT and
+ * UPDATE respectively.
*/
static void
addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
Relation pkrel, Oid indexOid, Oid parentConstr,
int numfks, int16 *pkattnum, int16 *fkattnum,
Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators,
- bool old_check_ok, LOCKMODE lockmode)
+ bool old_check_ok, LOCKMODE lockmode,
+ Oid parentInsTrigger, Oid parentUpdTrigger)
{
+ Oid insertTriggerOid,
+ updateTriggerOid;
+
AssertArg(OidIsValid(parentConstr));
if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
@@ -9101,19 +9133,21 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
errmsg("foreign key constraints are not supported on foreign tables")));
/*
- * If the referencing relation is a plain table, add the check triggers to
- * it and, if necessary, schedule it to be checked in Phase 3.
+ * Add the check triggers to it and, if necessary, schedule it to be
+ * checked in Phase 3.
*
* If the relation is partitioned, drill down to do it to its partitions.
*/
+ createForeignKeyCheckTriggers(RelationGetRelid(rel),
+ RelationGetRelid(pkrel),
+ fkconstraint,
+ parentConstr,
+ indexOid,
+ parentInsTrigger, parentUpdTrigger,
+ &insertTriggerOid, &updateTriggerOid);
+
if (rel->rd_rel->relkind == RELKIND_RELATION)
{
- createForeignKeyCheckTriggers(RelationGetRelid(rel),
- RelationGetRelid(pkrel),
- fkconstraint,
- parentConstr,
- indexOid);
-
/*
* Tell Phase 3 to check that the constraint is satisfied by existing
* rows. We can skip this during table creation, when requested
@@ -9142,6 +9176,15 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
PartitionDesc pd = RelationGetPartitionDesc(rel);
+ Relation trigrel;
+
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times
+ * in the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines
+ * called below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
* Recurse to take appropriate action on each partition; either we
@@ -9183,7 +9226,10 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
numfks,
mapped_fkattnum,
pkattnum,
- pfeqoperators))
+ pfeqoperators,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
break;
@@ -9262,10 +9308,14 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
ppeqoperators,
ffeqoperators,
old_check_ok,
- lockmode);
+ lockmode,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(partition, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
}
@@ -9321,6 +9371,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
ScanKeyData key[2];
HeapTuple tuple;
List *clone = NIL;
+ Relation trigrel;
/*
* Search for any constraints where this partition's parent is in the
@@ -9350,6 +9401,14 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
systable_endscan(scan);
table_close(pg_constraint, RowShareLock);
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
attmap = build_attrmap_by_name(RelationGetDescr(partitionRel),
RelationGetDescr(parentRel));
foreach(cell, clone)
@@ -9367,6 +9426,8 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
Oid conppeqop[INDEX_MAX_KEYS];
Oid conffeqop[INDEX_MAX_KEYS];
Constraint *fkconstraint;
+ Oid deleteTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, constrOid);
if (!HeapTupleIsValid(tuple))
@@ -9433,6 +9494,16 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
if (!OidIsValid(partIndexId))
elog(ERROR, "index for %u not found in partition %s",
indexOid, RelationGetRelationName(partitionRel));
+
+ /*
+ * Get the "action" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferenced().
+ */
+ GetForeignKeyActionTriggers(trigrel, constrOid,
+ constrForm->confrelid, constrForm->conrelid,
+ &deleteTriggerOid, &updateTriggerOid);
+
addFkRecurseReferenced(NULL,
fkconstraint,
fkRel,
@@ -9445,11 +9516,15 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel)
conpfeqop,
conppeqop,
conffeqop,
- true);
+ true,
+ deleteTriggerOid,
+ updateTriggerOid);
table_close(fkRel, NoLock);
ReleaseSysCache(tuple);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9472,6 +9547,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
List *partFKs;
List *clone = NIL;
ListCell *cell;
+ Relation trigrel;
/* obtain a list of constraints that we need to clone */
foreach(cell, RelationGetFKeyList(parentRel))
@@ -9493,6 +9569,14 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("foreign key constraints are not supported on foreign tables")));
+ /*
+ * Triggers of the foreign keys will be manipulated a bunch of times in
+ * the loop below. To avoid repeatedly opening/closing the trigger
+ * catalog relation, we open it here and pass it to the subroutines called
+ * below.
+ */
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
/*
* The constraint key may differ, if the columns in the partition are
* different. This map is used to convert them.
@@ -9522,6 +9606,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
ObjectAddress address,
referenced;
ListCell *cell;
+ Oid insertTriggerOid,
+ updateTriggerOid;
tuple = SearchSysCache1(CONSTROID, parentConstrOid);
if (!HeapTupleIsValid(tuple))
@@ -9550,6 +9636,19 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
for (int i = 0; i < numfks; i++)
mapped_conkey[i] = attmap->attnums[conkey[i] - 1];
+ /*
+ * Get the "check" triggers belonging to the constraint to pass as
+ * parent OIDs for similar triggers that will be created on the
+ * partition in addFkRecurseReferencing(). They are also passed to
+ * tryAttachPartitionForeignKey() below to simply assign as parents to
+ * the partition's existing "check" triggers, that is, if the
+ * corresponding constraints is deemed attachable to the parent
+ * constraint.
+ */
+ GetForeignKeyCheckTriggers(trigrel, constrForm->oid,
+ constrForm->confrelid, constrForm->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+
/*
* Before creating a new constraint, see whether any existing FKs are
* fit for the purpose. If one is, attach the parent constraint to
@@ -9568,7 +9667,10 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
numfks,
mapped_conkey,
confkey,
- conpfeqop))
+ conpfeqop,
+ insertTriggerOid,
+ updateTriggerOid,
+ trigrel))
{
attached = true;
table_close(pkrel, NoLock);
@@ -9667,9 +9769,13 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
conppeqop,
conffeqop,
false, /* no old check exists */
- AccessExclusiveLock);
+ AccessExclusiveLock,
+ insertTriggerOid,
+ updateTriggerOid);
table_close(pkrel, NoLock);
}
+
+ table_close(trigrel, RowExclusiveLock);
}
/*
@@ -9690,16 +9796,20 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
int numfks,
AttrNumber *mapped_conkey,
AttrNumber *confkey,
- Oid *conpfeqop)
+ Oid *conpfeqop,
+ Oid parentInsTrigger,
+ Oid parentUpdTrigger,
+ Relation trigrel)
{
HeapTuple parentConstrTup;
Form_pg_constraint parentConstr;
HeapTuple partcontup;
Form_pg_constraint partConstr;
- Relation trigrel;
ScanKeyData key;
SysScanDesc scan;
HeapTuple trigtup;
+ Oid insertTriggerOid,
+ updateTriggerOid;
parentConstrTup = SearchSysCache1(CONSTROID,
ObjectIdGetDatum(parentConstrOid));
@@ -9760,12 +9870,10 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
* in the partition. We identify them because they have our constraint
* OID, as well as being on the referenced rel.
*/
- trigrel = table_open(TriggerRelationId, RowExclusiveLock);
ScanKeyInit(&key,
Anum_pg_trigger_tgconstraint,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(fk->conoid));
-
scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
NULL, 1, &key);
while ((trigtup = systable_getnext(scan)) != NULL)
@@ -9798,13 +9906,136 @@ tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk,
}
systable_endscan(scan);
- table_close(trigrel, RowExclusiveLock);
ConstraintSetParentConstraint(fk->conoid, parentConstrOid, partRelid);
+
+ /*
+ * Like the constraint, attach partition's "check" triggers to the
+ * corresponding parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid) && OidIsValid(parentInsTrigger));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, parentInsTrigger,
+ partRelid);
+ Assert(OidIsValid(updateTriggerOid) && OidIsValid(parentUpdTrigger));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, parentUpdTrigger,
+ partRelid);
+
CommandCounterIncrement();
return true;
}
+/*
+ * GetForeignKeyActionTriggers
+ * Returns delete and update "action" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyActionTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *deleteTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *deleteTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != conrelid)
+ continue;
+ if (trgform->tgrelid != confrelid)
+ continue;
+ if (TRIGGER_FOR_DELETE(trgform->tgtype))
+ {
+ Assert(*deleteTriggerOid == InvalidOid);
+ *deleteTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+ if (OidIsValid(*deleteTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*deleteTriggerOid))
+ elog(ERROR, "could not find ON DELETE action trigger of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE action trigger of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
+
+/*
+ * GetForeignKeyCheckTriggers
+ * Returns insert and update "check" triggers of the given relation
+ * belonging to the given constraint
+ */
+static void
+GetForeignKeyCheckTriggers(Relation trigrel,
+ Oid conoid, Oid confrelid, Oid conrelid,
+ Oid *insertTriggerOid,
+ Oid *updateTriggerOid)
+{
+ ScanKeyData key;
+ SysScanDesc scan;
+ HeapTuple trigtup;
+
+ *insertTriggerOid = *updateTriggerOid = InvalidOid;
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgconstraint,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(conoid));
+
+ scan = systable_beginscan(trigrel, TriggerConstraintIndexId, true,
+ NULL, 1, &key);
+ while ((trigtup = systable_getnext(scan)) != NULL)
+ {
+ Form_pg_trigger trgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+
+ if (trgform->tgconstrrelid != confrelid)
+ continue;
+ if (trgform->tgrelid != conrelid)
+ continue;
+ if (TRIGGER_FOR_INSERT(trgform->tgtype))
+ {
+ Assert(*insertTriggerOid == InvalidOid);
+ *insertTriggerOid = trgform->oid;
+ }
+ else if (TRIGGER_FOR_UPDATE(trgform->tgtype))
+ {
+ Assert(*updateTriggerOid == InvalidOid);
+ *updateTriggerOid = trgform->oid;
+ }
+ if (OidIsValid(*insertTriggerOid) && OidIsValid(*updateTriggerOid))
+ break;
+ }
+
+ if (!OidIsValid(*insertTriggerOid))
+ elog(ERROR, "could not find ON INSERT check triggers of foreign key constraint %u",
+ conoid);
+ if (!OidIsValid(*updateTriggerOid))
+ elog(ERROR, "could not find ON UPDATE check triggers of foreign key constraint %u",
+ conoid);
+
+ systable_endscan(scan);
+}
/*
* ALTER TABLE ALTER CONSTRAINT
@@ -10721,10 +10952,19 @@ validateForeignKeyConstraint(char *conname,
ExecDropSingleTupleTableSlot(slot);
}
-static void
+/*
+ * CreateFKCheckTrigger
+ * Creates the insert (on_insert=true) or update "check" trigger that
+ * implements a given foreign key
+ *
+ * Returns the OID of the so created trigger.
+ */
+static Oid
CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid, bool on_insert)
+ Oid constraintOid, Oid indexOid, Oid parentTrigOid,
+ bool on_insert)
{
+ ObjectAddress trigAddress;
CreateTrigStmt *fk_trigger;
/*
@@ -10763,23 +11003,32 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->constrrel = NULL;
fk_trigger->args = NIL;
- (void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
+ constraintOid, indexOid, InvalidOid,
+ parentTrigOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
+
+ return trigAddress.objectId;
}
/*
* createForeignKeyActionTriggers
* Create the referenced-side "action" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *deleteTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
- Oid constraintOid, Oid indexOid)
+ Oid constraintOid, Oid indexOid,
+ Oid parentDelTrigger, Oid parentUpdTrigger,
+ Oid *deleteTrigOid, Oid *updateTrigOid)
{
CreateTrigStmt *fk_trigger;
+ ObjectAddress trigAddress;
/*
* Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
@@ -10830,9 +11079,12 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
}
fk_trigger->args = NIL;
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentDelTrigger, NULL, true, false);
+ if (deleteTrigOid)
+ *deleteTrigOid = trigAddress.objectId;
/* Make changes-so-far visible */
CommandCounterIncrement();
@@ -10886,25 +11138,35 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr
}
fk_trigger->args = NIL;
- (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel),
- constraintOid,
- indexOid, InvalidOid, InvalidOid, NULL, true, false);
+ trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid,
+ RelationGetRelid(rel),
+ constraintOid, indexOid, InvalidOid,
+ parentUpdTrigger, NULL, true, false);
+ if (updateTrigOid)
+ *updateTrigOid = trigAddress.objectId;
}
/*
* createForeignKeyCheckTriggers
* Create the referencing-side "check" triggers that implement a foreign
* key.
+ *
+ * Returns the OIDs of the so created triggers in *insertTrigOid and
+ * *updateTrigOid.
*/
static void
createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
Constraint *fkconstraint, Oid constraintOid,
- Oid indexOid)
+ Oid indexOid,
+ Oid parentInsTrigger, Oid parentUpdTrigger,
+ Oid *insertTrigOid, Oid *updateTrigOid)
{
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, true);
- CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
- indexOid, false);
+ *insertTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentInsTrigger, true);
+ *updateTrigOid = CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint,
+ constraintOid, indexOid,
+ parentUpdTrigger, false);
}
/*
@@ -17086,19 +17348,10 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
continue;
/*
- * Internal triggers require careful examination. Ideally, we don't
- * clone them. However, if our parent is itself a partition, there
- * might be internal triggers that must not be skipped; for example,
- * triggers on our parent that are in turn clones from its parent (our
- * grandparent) are marked internal, yet they are to be cloned.
- *
- * Note we dare not verify that the other trigger belongs to an
- * ancestor relation of our parent, because that creates deadlock
- * opportunities.
+ * Don't clone internal triggers, because the constraint cloning code
+ * will.
*/
- if (trigForm->tgisinternal &&
- (!parent->rd_rel->relispartition ||
- !OidIsValid(trigForm->tgparentid)))
+ if (trigForm->tgisinternal)
continue;
/*
@@ -17217,6 +17470,7 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
List *indexes;
List *fks;
ListCell *cell;
+ Relation trigrel = NULL;
/*
* We must lock the default partition, because detaching this partition
@@ -17309,12 +17563,16 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
* additional action triggers.
*/
fks = copyObject(RelationGetFKeyList(partRel));
+ if (fks != NIL)
+ trigrel = table_open(TriggerRelationId, RowExclusiveLock);
foreach(cell, fks)
{
ForeignKeyCacheInfo *fk = lfirst(cell);
HeapTuple contup;
Form_pg_constraint conform;
Constraint *fkconstraint;
+ Oid insertTriggerOid,
+ updateTriggerOid;
contup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(fk->conoid));
if (!HeapTupleIsValid(contup))
@@ -17332,6 +17590,20 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
/* unset conparentid and adjust conislocal, coninhcount, etc. */
ConstraintSetParentConstraint(fk->conoid, InvalidOid, InvalidOid);
+ /*
+ * Also, look up the partition's "check" triggers corresponding to the
+ * constraint being detached and detach them from the parent triggers.
+ */
+ GetForeignKeyCheckTriggers(trigrel,
+ fk->conoid, fk->confrelid, fk->conrelid,
+ &insertTriggerOid, &updateTriggerOid);
+ Assert(OidIsValid(insertTriggerOid));
+ TriggerSetParentTrigger(trigrel, insertTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+ Assert(OidIsValid(updateTriggerOid));
+ TriggerSetParentTrigger(trigrel, updateTriggerOid, InvalidOid,
+ RelationGetRelid(partRel));
+
/*
* Make the action triggers on the referenced relation. When this was
* a partition the action triggers pointed to the parent rel (they
@@ -17346,11 +17618,15 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
createForeignKeyActionTriggers(partRel, conform->confrelid,
fkconstraint, fk->conoid,
- conform->conindid);
+ conform->conindid,
+ InvalidOid, InvalidOid,
+ NULL, NULL);
ReleaseSysCache(contup);
}
list_free_deep(fks);
+ if (trigrel)
+ table_close(trigrel, RowExclusiveLock);
/*
* Any sub-constraints that are in the referenced-side of a larger
@@ -17441,6 +17717,14 @@ DropClonedTriggersFromPartition(Oid partitionId)
if (!OidIsValid(pg_trigger->tgparentid))
continue;
+ /*
+ * Ignore internal triggers that are implementation objects of foreign
+ * keys, because these will be detached when the foreign keys
+ * themselves are.
+ */
+ if (OidIsValid(pg_trigger->tgconstrrelid))
+ continue;
+
/*
* This is ugly, but necessary: remove the dependency markings on the
* trigger so that it can be removed.
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 788b92c7b8..c9d7702318 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -125,8 +125,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* given, stmt->funcname is ignored.
*
* parentTriggerOid, if nonzero, is a trigger that begets this one; so that
- * if that trigger is dropped, this one should be too. (This is passed as
- * Invalid by most callers; it's set here when recursing on a partition.)
+ * if that trigger is dropped, this one should be too. There are two cases
+ * when a nonzero value is passed for this: 1) when this function recurses to
+ * create the trigger on partitions, 2) when creating child foreign key
+ * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
*
* If whenClause is passed, it is an already-transformed expression for
* WHEN. In this case, we ignore any that may come in stmt->whenClause.
@@ -809,7 +811,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
values[Anum_pg_trigger_tgfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_trigger_tgtype - 1] = Int16GetDatum(tgtype);
values[Anum_pg_trigger_tgenabled - 1] = trigger_fires_when;
- values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal || in_partition);
+ values[Anum_pg_trigger_tgisinternal - 1] = BoolGetDatum(isInternal);
values[Anum_pg_trigger_tgconstrrelid - 1] = ObjectIdGetDatum(constrrelid);
values[Anum_pg_trigger_tgconstrindid - 1] = ObjectIdGetDatum(indexOid);
values[Anum_pg_trigger_tgconstraint - 1] = ObjectIdGetDatum(constraintOid);
@@ -1161,6 +1163,82 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
return myself;
}
+/*
+ * TriggerSetParentTrigger
+ * Set a partition's trigger as child of its parent trigger,
+ * or remove the linkage if parentTrigId is InvalidOid.
+ *
+ * This updates the constraint's pg_trigger row to show it as inherited, and
+ * adds PARTITION dependencies to prevent the trigger from being deleted
+ * on its own. Alternatively, reverse that.
+ */
+void
+TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId)
+{
+ SysScanDesc tgscan;
+ ScanKeyData skey[1];
+ Form_pg_trigger trigForm;
+ HeapTuple tuple,
+ newtup;
+ ObjectAddress depender;
+ ObjectAddress referenced;
+
+ /*
+ * Find the trigger to delete.
+ */
+ ScanKeyInit(&skey[0],
+ Anum_pg_trigger_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(childTrigId));
+
+ tgscan = systable_beginscan(trigRel, TriggerOidIndexId, true,
+ NULL, 1, skey);
+
+ tuple = systable_getnext(tgscan);
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for trigger %u", childTrigId);
+ newtup = heap_copytuple(tuple);
+ trigForm = (Form_pg_trigger) GETSTRUCT(newtup);
+ if (OidIsValid(parentTrigId))
+ {
+ /* don't allow setting parent for a constraint that already has one */
+ if (OidIsValid(trigForm->tgparentid))
+ elog(ERROR, "trigger %u already has a parent trigger",
+ childTrigId);
+
+ trigForm->tgparentid = parentTrigId;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ ObjectAddressSet(depender, TriggerRelationId, childTrigId);
+
+ ObjectAddressSet(referenced, TriggerRelationId, parentTrigId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_PRI);
+
+ ObjectAddressSet(referenced, RelationRelationId, childTableId);
+ recordDependencyOn(&depender, &referenced, DEPENDENCY_PARTITION_SEC);
+ }
+ else
+ {
+ trigForm->tgparentid = InvalidOid;
+
+ CatalogTupleUpdate(trigRel, &tuple->t_self, newtup);
+
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ TriggerRelationId,
+ DEPENDENCY_PARTITION_PRI);
+ deleteDependencyRecordsForClass(TriggerRelationId, childTrigId,
+ RelationRelationId,
+ DEPENDENCY_PARTITION_SEC);
+ }
+
+ heap_freetuple(newtup);
+ systable_endscan(tgscan);
+}
+
/*
* Guts of trigger deletion.
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 147a860b9b..796901450e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -7844,7 +7844,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid,
i_tgconstrrelname,
i_tgenabled,
- i_tgisinternal,
+ i_tgispartition,
i_tgdeferrable,
i_tginitdeferred,
i_tgdef;
@@ -7870,18 +7870,20 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
* could result in non-forward-compatible dumps of WHEN clauses
* due to under-parenthesization.
*
- * NB: We need to see tgisinternal triggers in partitions, in case
- * the tgenabled flag has been changed from the parent.
+ * NB: We need to see partition triggers in case the tgenabled flag
+ * has been changed from the parent.
*/
appendPQExpBuffer(query,
"SELECT t.tgname, "
"t.tgfoid::pg_catalog.regproc AS tgfname, "
"pg_catalog.pg_get_triggerdef(t.oid, false) AS tgdef, "
- "t.tgenabled, t.tableoid, t.oid, t.tgisinternal "
+ "t.tgenabled, t.tableoid, t.oid, "
+ "t.tgparentid <> 0 AS tgispartition\n"
"FROM pg_catalog.pg_trigger t "
"LEFT JOIN pg_catalog.pg_trigger u ON u.oid = t.tgparentid "
"WHERE t.tgrelid = '%u'::pg_catalog.oid "
- "AND (NOT t.tgisinternal OR t.tgenabled != u.tgenabled)",
+ "AND ((NOT t.tgisinternal AND t.tgparentid = 0) "
+ "OR t.tgenabled != u.tgenabled) ",
tbinfo->dobj.catId.oid);
}
else if (fout->remoteVersion >= 110000)
@@ -7984,7 +7986,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
i_tgconstrrelid = PQfnumber(res, "tgconstrrelid");
i_tgconstrrelname = PQfnumber(res, "tgconstrrelname");
i_tgenabled = PQfnumber(res, "tgenabled");
- i_tgisinternal = PQfnumber(res, "tgisinternal");
+ i_tgispartition = PQfnumber(res, "tgispartition");
i_tgdeferrable = PQfnumber(res, "tgdeferrable");
i_tginitdeferred = PQfnumber(res, "tginitdeferred");
i_tgdef = PQfnumber(res, "tgdef");
@@ -8004,7 +8006,7 @@ getTriggers(Archive *fout, TableInfo tblinfo[], int numTables)
tginfo[j].dobj.namespace = tbinfo->dobj.namespace;
tginfo[j].tgtable = tbinfo;
tginfo[j].tgenabled = *(PQgetvalue(res, j, i_tgenabled));
- tginfo[j].tgisinternal = *(PQgetvalue(res, j, i_tgisinternal)) == 't';
+ tginfo[j].tgispartition = *(PQgetvalue(res, j, i_tgispartition)) == 't';
if (i_tgdef >= 0)
{
tginfo[j].tgdef = pg_strdup(PQgetvalue(res, j, i_tgdef));
@@ -17753,11 +17755,13 @@ dumpTrigger(Archive *fout, TriggerInfo *tginfo)
"pg_catalog.pg_trigger", "TRIGGER",
trigidentity->data);
- if (tginfo->tgisinternal)
+ if (tginfo->tgispartition)
{
+ Assert(tbinfo->ispartition);
+
/*
- * Triggers marked internal only appear here because their 'tgenabled'
- * flag differs from its parent's. The trigger is created already, so
+ * Partition triggers only appear here because their 'tgenabled' flag
+ * differs from its parent's. The trigger is created already, so
* remove the CREATE and replace it with an ALTER. (Clear out the
* DROP query too, so that pg_dump --create does not cause errors.)
*/
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 7d41064b8a..4db409a328 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -415,7 +415,7 @@ typedef struct _triggerInfo
Oid tgconstrrelid;
char *tgconstrrelname;
char tgenabled;
- bool tgisinternal;
+ bool tgispartition;
bool tgdeferrable;
bool tginitdeferred;
char *tgdef;
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 384db60878..c16cc1938e 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2969,10 +2969,20 @@ describeOneTableDetails(const char *schemaname,
" AND u.tgparentid = 0) AS parent" :
"NULL AS parent"),
oid);
- if (pset.sversion >= 110000)
+
+ /*
+ * tgisinternal is set true for inherited triggers of partitions in
+ * servers betweem v11 and v14, though these must still be shown to
+ * the user. So we use another property that is true for such
+ * inherited triggers to avoid them being hidden, which is their
+ * dependendence on another trigger; must still hide the internal
+ * triggers thar originate from a constraint.
+ */
+ if (pset.sversion >= 110000 && pset.sversion < 150000)
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"
- " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass))");
+ " OR (EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE objid = t.oid \n"
+ " AND refclassid = 'pg_catalog.pg_trigger'::pg_catalog.regclass) \n"
+ " AND t.tgconstraint = 0))");
else if (pset.sversion >= 90000)
/* display/warn about disabled internal triggers */
appendPQExpBufferStr(&buf, "(NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))");
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 40b8154876..1873ce14da 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -168,6 +168,10 @@ extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *que
Node *whenClause, bool isInternal, bool in_partition,
char trigger_fires_when);
+extern void TriggerSetParentTrigger(Relation trigRel,
+ Oid childTrigId,
+ Oid parentTrigId,
+ Oid childTableId);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 207e0d3779..397f7763a0 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2105,7 +2105,7 @@ select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal
tgrelid | tgname | tgfoid | tgenabled | tgisinternal
-----------+--------+-----------------+-----------+--------------
trigpart | trg1 | trigger_nothing | O | f
- trigpart1 | trg1 | trigger_nothing | O | t
+ trigpart1 | trg1 | trigger_nothing | O | f
(2 rows)
create table trigpart3 (like trigpart);
--
2.24.1
13_v12-0002-Enforce-foreign-key-correctly-during-cross-partition.patch.txttext/plain; charset=US-ASCII; name=13_v12-0002-Enforce-foreign-key-correctly-during-cross-partition.patch.txtDownload
From bfdd0f75ae43dd201323f0f96c0227398db083ab Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH 2/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 322 +++++++++++++++++++---
src/backend/executor/execMain.c | 20 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/execUtils.c | 26 ++
src/backend/executor/nodeModifyTable.c | 173 +++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 4 +
src/include/executor/executor.h | 4 +-
src/include/nodes/execnodes.h | 17 ++
src/test/regress/expected/foreign_key.out | 216 ++++++++++++++-
src/test/regress/sql/foreign_key.sql | 147 ++++++++++
12 files changed, 900 insertions(+), 47 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c9d7702318..acaf2bc97b 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -87,7 +87,11 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2249,7 +2253,9 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2338,7 +2344,9 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2463,7 +2471,9 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2560,7 +2570,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2583,7 +2594,9 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2704,7 +2717,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -2840,8 +2855,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
+ * destination partitions, respectively, of a cross-partition update of the
+ * root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey().
+ */
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -2849,7 +2876,10 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
TransitionCaptureState *transition_capture)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
+ src_partinfo != NULL ?
+ src_partinfo :
+ relinfo);
ExecClearTuple(oldslot);
@@ -2867,7 +2897,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ src_partinfo != NULL ? src_partinfo : relinfo,
tupleid,
LockTupleExclusive,
oldslot,
@@ -2875,7 +2905,9 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else if (fdw_trigtuple != NULL)
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3001,7 +3033,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3318,7 +3352,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
@@ -3326,7 +3360,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_FDW_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0xC8000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -3347,8 +3382,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to
+ * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
+ ItemPointerData ate_ctid2; /* new updated tuple */
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3363,11 +3414,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3549,6 +3602,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -3873,8 +3928,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -3888,6 +3951,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -3895,6 +3960,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -3975,12 +4042,36 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -3990,16 +4081,42 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4228,13 +4345,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4259,12 +4379,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5458,16 +5599,38 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
+ Relation rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
+ bool maybe_crosspart_update =
+ (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)));
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
@@ -5574,6 +5737,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partitioned update. In that
+ * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5634,6 +5810,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5660,11 +5846,43 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
+ AFTER_TRIGGER_2CTID) :
+ AFTER_TRIGGER_1CTID;
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the loop
+ * below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5694,12 +5912,28 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
* tell by inspection that the FK constraint will still pass.
+ * There are also some cases during cross-partition updates of a
+ * partitioned table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the update
+ * event that will be fired on the root (partitioned)
+ * target table will be used to perform the necessary
+ * foreign key enforcement action.
+ */
+ if (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5710,8 +5944,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5720,7 +5965,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a partitioned
+ * table, because the same row trigger must be present in
+ * the leaf partition(s) that are affected as part of this
+ * update and the events fired on them are queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 565262dd27..46a2be586f 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1354,7 +1354,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
int nr;
@@ -1419,7 +1420,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1552,7 +1553,22 @@ ExecEndPlan(PlanState *planstate, EState *estate)
resultRelInfo = estate->es_result_relations;
for (i = estate->es_num_result_relations; i > 0; i--)
{
+ ListCell *lc;
+
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
resultRelInfo++;
}
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 8f474faed0..080fda42be 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -512,7 +512,8 @@ ExecSimpleRelationUpdate(EState *estate, EPQState *epqstate,
NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -552,7 +553,7 @@ ExecSimpleRelationDelete(EState *estate, EPQState *epqstate,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index fe4925f506..b7330f4594 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -1205,6 +1205,32 @@ ExecGetReturningSlot(EState *estate, ResultRelInfo *relInfo)
return relInfo->ri_ReturningSlot;
}
+/*
+ * Return the map needed to convert given child result relation's tuples to
+ * the rowtype of the query's main target ("root") relation. Note that a
+ * NULL result is valid and means that no conversion is needed.
+ */
+TupleConversionMap *
+ExecGetChildToRootMap(ResultRelInfo *resultRelInfo)
+{
+ /* If we didn't already do so, compute the map for this child. */
+ if (!resultRelInfo->ri_ChildToRootMapValid)
+ {
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+
+ if (rootRelInfo)
+ resultRelInfo->ri_ChildToRootMap =
+ convert_tuples_by_name(RelationGetDescr(resultRelInfo->ri_RelationDesc),
+ RelationGetDescr(rootRelInfo->ri_RelationDesc));
+ else /* this isn't a child result rel */
+ resultRelInfo->ri_ChildToRootMap = NULL;
+
+ resultRelInfo->ri_ChildToRootMapValid = true;
+ }
+
+ return resultRelInfo->ri_ChildToRootMap;
+}
+
/* Return a bitmap representing columns being inserted */
Bitmapset *
ExecGetInsertedCols(ResultRelInfo *relinfo, EState *estate)
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index c5d1c92432..811d7f667b 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -42,6 +42,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -394,7 +395,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *srcSlot,
ResultRelInfo *returningRelInfo,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
ResultRelInfo *resultRelInfo;
Relation resultRelationDesc;
@@ -666,7 +669,9 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
@@ -741,6 +746,11 @@ ExecInsert(ModifyTableState *mtstate,
slot, planSlot);
}
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1041,7 +1051,8 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
@@ -1056,7 +1067,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1106,6 +1117,124 @@ ldelete:;
return NULL;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's trigger
+ * to check that a cross-partition update hasn't broken any foreign keys
+ * pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
+ sourcePartInfo, destPartInfo,
+ tupleid, NULL,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1272,11 +1401,13 @@ lreplace:;
{
bool tuple_deleted;
TupleTableSlot *ret_slot;
+ TupleTableSlot *inserted_tuple;
TupleTableSlot *orig_slot = slot;
TupleTableSlot *epqslot = NULL;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
int map_index;
TupleConversionMap *tupconv_map;
+ ResultRelInfo *insert_destrel = NULL;
/*
* Disallow an INSERT ON CONFLICT DO UPDATE that causes the
@@ -1378,9 +1509,11 @@ lreplace:;
slot = ExecPrepareTupleRouting(mtstate, estate, proute,
mtstate->rootResultRelInfo, slot);
+ /* Tuple routing starts from the root table. */
ret_slot = ExecInsert(mtstate, slot, planSlot,
orig_slot, resultRelInfo,
- estate, canSetTag);
+ estate, canSetTag,
+ &inserted_tuple, &insert_destrel);
/* Revert ExecPrepareTupleRouting's node change. */
estate->es_result_relation_info = resultRelInfo;
@@ -1390,6 +1523,29 @@ lreplace:;
mtstate->mt_transition_capture->tcs_map = saved_tcs_map;
}
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ insert_destrel,
+ tupleid, orig_slot,
+ inserted_tuple,
+ mtstate, estate);
+
return ret_slot;
}
@@ -1554,7 +1710,10 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2320,7 +2479,7 @@ ExecModifyTable(PlanState *pstate)
resultRelInfo, slot);
slot = ExecInsert(node, slot, planSlot,
NULL, estate->es_result_relation_info,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
/* Revert ExecPrepareTupleRouting's state change. */
if (proute)
estate->es_result_relation_info = resultRelInfo;
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 47d4965215..60f448cdb1 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1200,6 +1200,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 1873ce14da..04187bc8ae 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -219,6 +219,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -238,7 +239,10 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 18f834fddc..df147157e4 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -193,7 +193,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecCleanUpTriggerState(EState *estate);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
@@ -576,6 +577,7 @@ extern int ExecCleanTargetListLength(List *targetlist);
extern TupleTableSlot *ExecGetTriggerOldSlot(EState *estate, ResultRelInfo *relInfo);
extern TupleTableSlot *ExecGetTriggerNewSlot(EState *estate, ResultRelInfo *relInfo);
extern TupleTableSlot *ExecGetReturningSlot(EState *estate, ResultRelInfo *relInfo);
+extern TupleConversionMap *ExecGetChildToRootMap(ResultRelInfo *resultRelInfo);
extern Bitmapset *ExecGetInsertedCols(ResultRelInfo *relinfo, EState *estate);
extern Bitmapset *ExecGetUpdatedCols(ResultRelInfo *relinfo, EState *estate);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index a9a45f566c..0895871747 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -499,6 +499,23 @@ typedef struct ResultRelInfo
/* For use by copy.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+
+ /*
+ * Map to convert child result relation tuples to the format of the table
+ * actually mentioned in the query (called "root"). Computed only if
+ * needed. A NULL map value indicates that no conversion is needed, so we
+ * must have a separate flag to show if the map has been computed.
+ */
+ TupleConversionMap *ri_ChildToRootMap;
+ bool ri_ChildToRootMapValid;
+
+ /*
+ * List of ResultRelInfos a leaf partition's ancestors, including the root
+ * result relation.
+ */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 16698b7a07..56f50c2dc0 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2485,7 +2485,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2548,3 +2548,217 @@ DROP SCHEMA fkpart9 CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table fkpart9.pk
drop cascades to table fkpart9.fk
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+CREATE SCHEMA fkpart10
+ CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
+INSERT INTO fkpart10.tbl1 VALUES (0), (1);
+INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
+BEGIN;
+DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
+UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
+INSERT INTO fkpart10.tbl1 VALUES (0), (1);
+COMMIT;
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+DROP SCHEMA fkpart10 CASCADE;
+NOTICE: drop cascades to 5 other objects
+DETAIL: drop cascades to table fkpart10.tbl1
+drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index c52672e03a..a45e0f14f5 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1813,3 +1813,150 @@ DELETE FROM fkpart9.pk WHERE a=35;
SELECT * FROM fkpart9.pk;
SELECT * FROM fkpart9.fk;
DROP SCHEMA fkpart9 CASCADE;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+CREATE SCHEMA fkpart10
+ CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
+INSERT INTO fkpart10.tbl1 VALUES (0), (1);
+INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
+BEGIN;
+DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
+UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
+INSERT INTO fkpart10.tbl1 VALUES (0), (1);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
On 2022-Jan-17, Amit Langote wrote:
Note that the fix involves adding fields to ResultRelInfo -- v13 needs
2 additional, while v14 and HEAD need 1. That combined with needing
new catalog entries for parent FK triggers, back-patching this does
make me a bit uncomfortable.
Yeah, that's a good point, so I ran abidiff on the binaries in branch 13
to have some data on it. The report does indeed have a lot of noise
about those three added members in struct ResultRelInfo; but as far as I
can see in the report, there is no ABI affected because of these
changes.
However, the ones that caught my eye next were the ABI changes for
ExecGetTriggerResultRel() and ExecAR{Delete,Update}Triggers(). These seem more
significant, if any external code is calling these. Now, while I think
we could dodge that (at least part of it) by having a shim for
AfterTriggerSaveEvent that passes a NULL mtstate, and takes the
assumption that there is no row partition migration when that happens
... that seems like treading in dangerous territory: we would have
code that would behave differently for an extension that was compiled
with an earlier copy of the backend.
So I see two options. One is to introduce the aforementioned shim, but
instead of making any assumptions, we cause the shim raise an error:
"your extension is outdated, please recompile with the new postgres
version". However, that seems even more harmful, because production
systems that auto-update to the next Postgres version would start to
fail.
The other is suggested by you:
Another thing to consider is that we haven't seen many reports of the
problem (UPDATEs of partitioned PK tables causing DELETEs in
referencing tables), even though it can be possibly very surprising to
those who do run into it.
Do nothing in the old branches.
Another thing I saw which surprised me very much is this bit, which I
think must be a bug in abidiff:
type of 'EPQState* EState::es_epq_active' changed:
in pointed to type 'struct EPQState' at execnodes.h:1104:1:
type size hasn't changed
1 data member changes (3 filtered):
type of 'PlanState* EPQState::recheckplanstate' changed:
in pointed to type 'struct PlanState' at execnodes.h:1056:1:
entity changed from 'struct PlanState' to compatible type 'typedef PlanState' at execnodes.h:1056:1
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
"La grandeza es una experiencia transitoria. Nunca es consistente.
Depende en gran parte de la imaginación humana creadora de mitos"
(Irulan)
On Mon, Jan 17, 2022 at 6:26 AM Alvaro Herrera <alvherre@alvh.no-ip.org>
wrote:
On 2022-Jan-17, Amit Langote wrote:
Note that the fix involves adding fields to ResultRelInfo -- v13 needs
2 additional, while v14 and HEAD need 1. That combined with needing
new catalog entries for parent FK triggers, back-patching this does
make me a bit uncomfortable.Yeah, that's a good point, so I ran abidiff on the binaries in branch 13
to have some data on it. The report does indeed have a lot of noise
about those three added members in struct ResultRelInfo; but as far as I
can see in the report, there is no ABI affected because of these
changes.However, the ones that caught my eye next were the ABI changes for
ExecGetTriggerResultRel() and ExecAR{Delete,Update}Triggers(). These seem
more
significant, if any external code is calling these. Now, while I think
we could dodge that (at least part of it) by having a shim for
AfterTriggerSaveEvent that passes a NULL mtstate, and takes the
assumption that there is no row partition migration when that happens
... that seems like treading in dangerous territory: we would have
code that would behave differently for an extension that was compiled
with an earlier copy of the backend.So I see two options. One is to introduce the aforementioned shim, but
instead of making any assumptions, we cause the shim raise an error:
"your extension is outdated, please recompile with the new postgres
version". However, that seems even more harmful, because production
systems that auto-update to the next Postgres version would start to
fail.The other is suggested by you:
Another thing to consider is that we haven't seen many reports of the
problem (UPDATEs of partitioned PK tables causing DELETEs in
referencing tables), even though it can be possibly very surprising to
those who do run into it.Do nothing in the old branches.
Another thing I saw which surprised me very much is this bit, which I
think must be a bug in abidiff:type of 'EPQState* EState::es_epq_active'
changed:
in pointed to type 'struct EPQState' at
execnodes.h:1104:1:
type size hasn't changed
1 data member changes (3 filtered):
type of 'PlanState*
EPQState::recheckplanstate' changed:
in pointed to type 'struct
PlanState' at execnodes.h:1056:1:
entity changed from 'struct
PlanState' to compatible type 'typedef PlanState' at execnodes.h:1056:1Hi,
I think option 2, not backpatching, is more desirable at this stage.
If, complaints from the field arise in the future, we can consider
backpatching.
Cheers
On 2022-Jan-17, Zhihong Yu wrote:
On Mon, Jan 17, 2022 at 6:26 AM Alvaro Herrera <alvherre@alvh.no-ip.org>
wrote:
On 2022-Jan-17, Amit Langote wrote:
The other is suggested by you:
Another thing to consider is that we haven't seen many reports of the
problem (UPDATEs of partitioned PK tables causing DELETEs in
referencing tables), even though it can be possibly very surprising to
those who do run into it.Do nothing in the old branches.
I think option 2, not backpatching, is more desirable at this stage.
Preliminarly, I tend to agree.
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
@@ -3398,7 +3432,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */ +#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */ #define AFTER_TRIGGER_DONE 0x10000000 #define AFTER_TRIGGER_IN_PROGRESS 0x20000000 /* bits describing the size and tuple sources of this event */ @@ -3406,7 +3440,8 @@ typedef uint32 TriggerFlags; #define AFTER_TRIGGER_FDW_FETCH 0x80000000 #define AFTER_TRIGGER_1CTID 0x40000000 #define AFTER_TRIGGER_2CTID 0xC0000000 -#define AFTER_TRIGGER_TUP_BITS 0xC0000000 +#define AFTER_TRIGGER_CP_UPDATE 0x08000000 +#define AFTER_TRIGGER_TUP_BITS 0xC8000000
So this patch releases one bit from AFTER_TRIGGER_OFFSET and makes it
become AFTER_TRIGGER_CP_UPDATE. As far as I can tell there is no harm
in doing so.
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
So this patch releases one bit from AFTER_TRIGGER_OFFSET and makes it
become AFTER_TRIGGER_CP_UPDATE. As far as I can tell there is no harm
in doing so.
I agree that taking a bit away from AFTER_TRIGGER_OFFSET is okay
(it could spare even a couple more, if we need them).
But could we please do it in a way that is designed to keep the
code readable, rather than to minimize the number of lines of diff?
It makes zero sense to have the bits in AFTER_TRIGGER_TUP_BITS not
be adjacent. So what should happen here is to renumber the symbols
in between to move their bits over one place.
(Since this data is only known within trigger.c, I don't even see
an ABI-stability argument for not changing these assignments.)
regards, tom lane
On 2022-Jan-17, Tom Lane wrote:
But could we please do it in a way that is designed to keep the
code readable, rather than to minimize the number of lines of diff?
It makes zero sense to have the bits in AFTER_TRIGGER_TUP_BITS not
be adjacent. So what should happen here is to renumber the symbols
in between to move their bits over one place.
Is it typical to enumerate bits starting from the right of each byte,
when doing it from the high bits of the word? DONE and IN_PROGRESS have
been defined as 0x1 and 0x2 of that byte for a very long time and I
found that very strange. I am inclined to count from the left, so I'd
pick 8 first, defining the set like this:
#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x80000000
#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
/* bits describing the size and tuple sources of this event */
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
#define AFTER_TRIGGER_FDW_FETCH 0x20000000
#define AFTER_TRIGGER_1CTID 0x10000000
#define AFTER_TRIGGER_2CTID 0x30000000
#define AFTER_TRIGGER_CP_UPDATE 0x08000000
#define AFTER_TRIGGER_TUP_BITS 0x38000000
(The fact that FDW_REUSE bits is actually an empty mask comes from
7cbe57c34dec, specifically [1]/messages/by-id/20140306033644.GA3527902@tornado.leadboat.com)
Is this what you were thinking?
[1]: /messages/by-id/20140306033644.GA3527902@tornado.leadboat.com
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
"All rings of power are equal,
But some rings of power are more equal than others."
(George Orwell's The Lord of the Rings)
Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
On 2022-Jan-17, Tom Lane wrote:
It makes zero sense to have the bits in AFTER_TRIGGER_TUP_BITS not
be adjacent. So what should happen here is to renumber the symbols
in between to move their bits over one place.
Is it typical to enumerate bits starting from the right of each byte,
when doing it from the high bits of the word? DONE and IN_PROGRESS have
been defined as 0x1 and 0x2 of that byte for a very long time and I
found that very strange. I am inclined to count from the left, so I'd
pick 8 first, defining the set like this:
Doesn't matter to me either way, as long as the values look like they
were all defined by the same person ;-)
(The fact that FDW_REUSE bits is actually an empty mask comes from
7cbe57c34dec, specifically [1])
That seemed a bit odd to me too, but this is not the patch to be
changing it in, I suppose.
regards, tom lane
On Tue, Jan 18, 2022 at 7:15 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Jan-17, Tom Lane wrote:
But could we please do it in a way that is designed to keep the
code readable, rather than to minimize the number of lines of diff?
It makes zero sense to have the bits in AFTER_TRIGGER_TUP_BITS not
be adjacent. So what should happen here is to renumber the symbols
in between to move their bits over one place.Is it typical to enumerate bits starting from the right of each byte,
when doing it from the high bits of the word? DONE and IN_PROGRESS have
been defined as 0x1 and 0x2 of that byte for a very long time and I
found that very strange. I am inclined to count from the left, so I'd
pick 8 first, defining the set like this:#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x80000000
#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
/* bits describing the size and tuple sources of this event */
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
#define AFTER_TRIGGER_FDW_FETCH 0x20000000
#define AFTER_TRIGGER_1CTID 0x10000000
#define AFTER_TRIGGER_2CTID 0x30000000
#define AFTER_TRIGGER_CP_UPDATE 0x08000000
#define AFTER_TRIGGER_TUP_BITS 0x38000000
Agree that the ultimate code readability trumps diff minimalism. :-)
Would you like me to update the patch with the above renumbering or
are you working on a new version yourself?
--
Amit Langote
EDB: http://www.enterprisedb.com
Hi,
On Mon, Jan 17, 2022 at 08:40:54PM +0900, Amit Langote wrote:
Okay, I created versions of the patch series for branches 13 and 14
(.txt files). The one for HEAD is also re-attached.
FYI The patch failed today on FreeBSD, while it was previously quite stable on
all platforms (https://cirrus-ci.com/build/4551468081479680), with this error:
https://api.cirrus-ci.com/v1/artifact/task/6360787076775936/regress_diffs/src/test/recovery/tmp_check/regression.diffs
diff -U3 /tmp/cirrus-ci-build/src/test/recovery/../regress/expected/reloptions.out /tmp/cirrus-ci-build/src/test/recovery/tmp_check/results/reloptions.out
--- /tmp/cirrus-ci-build/src/test/recovery/../regress/expected/reloptions.out 2022-01-18 00:12:52.533086000 +0000
+++ /tmp/cirrus-ci-build/src/test/recovery/tmp_check/results/reloptions.out 2022-01-18 00:28:00.000524000 +0000
@@ -133,7 +133,7 @@
SELECT pg_relation_size('reloptions_test') = 0;
?column?
----------
- t
+ f
(1 row)
I'm not sure why this test failed as it doesn't seem like something impacted by
the patch, but I may have missed something as I only had a quick look at the
patch and discussion.
On Tue, Jan 18, 2022 at 12:16:23PM +0800, Julien Rouhaud wrote:
I'm not sure why this test failed as it doesn't seem like something impacted by
the patch, but I may have missed something as I only had a quick look at the
patch and discussion.
This issue is discussed here:
/messages/by-id/20220117203746.oj43254j5jurbneu@alap3.anarazel.de
--
Michael
Hi,
On Tue, Jan 18, 2022 at 02:33:39PM +0900, Michael Paquier wrote:
On Tue, Jan 18, 2022 at 12:16:23PM +0800, Julien Rouhaud wrote:
I'm not sure why this test failed as it doesn't seem like something impacted by
the patch, but I may have missed something as I only had a quick look at the
patch and discussion.This issue is discussed here:
/messages/by-id/20220117203746.oj43254j5jurbneu@alap3.anarazel.de
Oh I missed it, thanks! Sorry for the noise.
On Tue, Jan 18, 2022 at 2:41 PM Julien Rouhaud <rjuju123@gmail.com> wrote:
On Tue, Jan 18, 2022 at 02:33:39PM +0900, Michael Paquier wrote:
On Tue, Jan 18, 2022 at 12:16:23PM +0800, Julien Rouhaud wrote:
I'm not sure why this test failed as it doesn't seem like something impacted by
the patch, but I may have missed something as I only had a quick look at the
patch and discussion.This issue is discussed here:
/messages/by-id/20220117203746.oj43254j5jurbneu@alap3.anarazel.deOh I missed it, thanks! Sorry for the noise.
Thanks, it had puzzled me too when I first saw it this morning.
--
Amit Langote
EDB: http://www.enterprisedb.com
On 2022-Jan-18, Amit Langote wrote:
Would you like me to update the patch with the above renumbering or
are you working on a new version yourself?
I have a few very minor changes apart from that. Let me see if I can
get this pushed soon; if I'm unable to (there are parts I don't fully
grok yet), I'll post what I have.
Thank you!
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
Julien Rouhaud <rjuju123@gmail.com> writes:
@@ -133,7 +133,7 @@ SELECT pg_relation_size('reloptions_test') = 0; ?column? ---------- - t + f (1 row)
Some machines have been showing that on HEAD too, so I doubt
it's the fault of this patch. That reloptions test isn't stable
yet seemingly.
regards, tom lane
On 2022-Jan-18, Alvaro Herrera wrote:
On 2022-Jan-18, Amit Langote wrote:
Would you like me to update the patch with the above renumbering or
are you working on a new version yourself?I have a few very minor changes apart from that. Let me see if I can
get this pushed soon; if I'm unable to (there are parts I don't fully
grok yet), I'll post what I have.
Here's v13, a series with your patch as 0001 and a few changes from me;
the bulk is just pgindent, and there are a few stylistic changes and an
unrelated typo fix and I added a couple of comments to your new code.
I don't like the API change to ExecInsert(). You're adding two output
arguments:
- the tuple being inserted. But surely you have this already, because
it's in the 'slot' argument you passed in. ExecInsert is even careful to
set the ->tts_tableOid argument there. So ExecInsert's caller doesn't
need to receive the inserted tuple as an argument, it can just read
'slot'.
- the relation to which the tuple was inserted. Can't this be obtained
by looking at slot->tts_tableOid? We should be able to use
ExecLookupResultRelByOid() to obtain it, no? (I suppose you may claim
that this is wasteful, but I think this is not a common case anyway and
it's worth keeping ExecInsert()'s API clean for the sake of the 99.99%
of its other calls).
I think the argument definition of ExecCrossPartitionUpdateForeignKey is
a bit messy. I propose to move mtstate,estate as two first arguments;
IIRC the whole executor does it that way.
AfterTriggerSaveEvent determines maybe_crosspart_update (by looking at
mtstate->operation -- why doesn't it look at 'event' instead?) and later
it determines new_event.ate_flags. Why can't it use
maybe_crosspart_update to simplify part of that? Or maybe the other way
around, not sure. It looks like something could be made simpler there.
Overall, the idea embodied in the patch looks sensible to me.
Thank you,
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
"Nunca confiaré en un traidor. Ni siquiera si el traidor lo he creado yo"
(Barón Vladimir Harkonnen)
Attachments:
v13-0001-Enforce-foreign-key-correctly-during-cross-parti.patchtext/x-diff; charset=utf-8Download
From 3f3dbdcf1228dd78285c4efcb4fc64f732408270 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH v13 1/2] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 322 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 187 ++++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 4 +
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 3 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++++-
src/test/regress/sql/foreign_key.sql | 135 ++++++++-
11 files changed, 840 insertions(+), 55 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 1a9c1ac290..0544efab56 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,7 +94,11 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate,
+ ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
@@ -2458,7 +2462,9 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2547,7 +2553,9 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
transition_capture);
@@ -2672,7 +2680,9 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
false, NULL, NULL, NIL, NULL, transition_capture);
}
@@ -2769,7 +2779,8 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
}
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TransitionCaptureState *transition_capture)
@@ -2793,7 +2804,9 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
transition_capture);
}
@@ -2914,7 +2927,9 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3052,8 +3067,20 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
+ * destination partitions, respectively, of a cross-partition update of the
+ * root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey().
+ */
void
-ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
@@ -3073,12 +3100,15 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
+ src_partinfo != NULL ?
+ src_partinfo :
+ relinfo);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ src_partinfo != NULL ? src_partinfo : relinfo,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,7 +3118,9 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
true, oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
transition_capture);
@@ -3214,7 +3246,9 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
+ AfterTriggerSaveEvent(estate, NULL, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
false, NULL, NULL, NIL, NULL, NULL);
}
@@ -3531,7 +3565,7 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
#define AFTER_TRIGGER_DONE 0x10000000
#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
/* bits describing the size and tuple sources of this event */
@@ -3539,7 +3573,8 @@ typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_FDW_FETCH 0x80000000
#define AFTER_TRIGGER_1CTID 0x40000000
#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0xC8000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
@@ -3560,8 +3595,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to
+ * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags; /* status bits and offset to shared data */
+ ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
+ ItemPointerData ate_ctid2; /* new updated tuple */
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3576,11 +3627,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3815,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4086,8 +4141,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4101,6 +4164,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4108,6 +4173,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4188,12 +4255,36 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4203,16 +4294,42 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4441,13 +4558,16 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4472,12 +4592,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5672,16 +5813,38 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
+ ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
+ Relation rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
+ bool maybe_crosspart_update =
+ (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind ==
+ RELKIND_PARTITIONED_TABLE)));
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
@@ -5788,6 +5951,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partitioned update. In that
+ * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5848,6 +6024,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5874,11 +6060,43 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
+ AFTER_TRIGGER_2CTID) :
+ AFTER_TRIGGER_1CTID;
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the loop
+ * below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5908,12 +6126,28 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
* tell by inspection that the FK constraint will still pass.
+ * There are also some cases during cross-partition updates of a
+ * partitioned table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the update
+ * event that will be fired on the root (partitioned)
+ * target table will be used to perform the necessary
+ * foreign key enforcement action.
+ */
+ if (maybe_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5924,8 +6158,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5934,7 +6179,16 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a partitioned
+ * table, because the same row trigger must be present in
+ * the leaf partition(s) that are affected as part of this
+ * update and the events fired on them are queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 549d9eb696..b6a2d4f708 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 313c87398b..e2a338ba33 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,7 +516,8 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
recheckIndexes, NULL);
@@ -556,7 +557,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo,
+ ExecARDeleteTriggers(estate, NULL, resultRelInfo,
tid, NULL, NULL);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 5ec699a9bd..7d32630030 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -596,7 +597,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,7 +959,9 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
@@ -994,6 +999,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1346,7 +1356,8 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
@@ -1361,7 +1372,7 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
ar_delete_trig_tcs);
/* Process RETURNING if present and if requested */
@@ -1433,7 +1444,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1569,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1584,124 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's trigger
+ * to check that a cross-partition update hasn't broken any foreign keys
+ * pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot,
+ ModifyTableState *mtstate,
+ EState *estate)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
+ sourcePartInfo, destPartInfo,
+ tupleid, NULL,
+ newslot, NIL, NULL);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1874,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1891,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in non-cross-partition
+ * update situations, because the leaf partition's AR update
+ * triggers will take care of that. During cross-partition
+ * updates implemented as delete on the source partition followed
+ * by insert on the destination partition, AR update triggers of
+ * the root table (that is, the table mentioned in the query) must
+ * be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple,
+ mtstate, estate);
+
+ return returning_slot;
}
/*
@@ -1942,7 +2102,10 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
@@ -2559,7 +2722,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index c95cd32402..01d4c22cfc 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index e1271420e5..1ba3a54499 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,6 +211,7 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
@@ -230,7 +231,10 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
+ ModifyTableState *mtstate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 344399f6a8..dcff6aeca5 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4ea8735dd8..6c7eef1e54 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -529,6 +529,9 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /* Used during cross-partition updates on partitioned tables. */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.30.2
v13-0002-alvherre-tweaks.patchtext/x-diff; charset=utf-8Download
From f0799ace07b785160d77634cb9e9ede9ab8cc0a5 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 18 Jan 2022 10:51:26 -0300
Subject: [PATCH v13 2/2] alvherre tweaks
---
src/backend/commands/trigger.c | 161 ++++++++++++++-----------
src/backend/executor/execMain.c | 2 +-
src/backend/executor/nodeModifyTable.c | 47 ++++----
src/include/executor/executor.h | 2 +-
4 files changed, 118 insertions(+), 94 deletions(-)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 0544efab56..e6aa36a9d8 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -3100,15 +3100,16 @@ ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate,
- src_partinfo != NULL ?
- src_partinfo :
- relinfo);
+ TupleTableSlot *oldslot;
+ ResultRelInfo *tupsrc;
+
+ tupsrc = src_partinfo ? src_partinfo : relinfo;
+ oldslot = ExecGetTriggerOldSlot(estate, tupsrc);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- src_partinfo != NULL ? src_partinfo : relinfo,
+ tupsrc,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3530,9 +3531,9 @@ typedef SetConstraintStateData *SetConstraintState;
* Per-trigger-event data
*
* The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS
- * status bits and up to two tuple CTIDs. Each event record also has an
- * associated AfterTriggerSharedData that is shared across all instances of
- * similar events within a "chunk".
+ * status bits, up to two tuple CTIDs, and optionally two OIDs of partitions
+ * Each event record also has an associated AfterTriggerSharedData that is
+ * shared across all instances of similar events within a "chunk".
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
@@ -3543,6 +3544,10 @@ typedef SetConstraintStateData *SetConstraintState;
* tuple(s). This permits storing tuples once regardless of the number of
* row-level triggers on a foreign table.
*
+ * When updates move tuples in partitioned tables to different partitions,
+ * the OIDs of both partitions are stored too, so that the tuples can be
+ * fetched.
+ *
* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
* the right order. To ensure that, we forbid deferrable (constraint)
@@ -3566,16 +3571,15 @@ typedef SetConstraintStateData *SetConstraintState;
typedef uint32 TriggerFlags;
#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
-#define AFTER_TRIGGER_DONE 0x10000000
-#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
+#define AFTER_TRIGGER_DONE 0x80000000
+#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
/* bits describing the size and tuple sources of this event */
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
-#define AFTER_TRIGGER_1CTID 0x40000000
-#define AFTER_TRIGGER_2CTID 0xC0000000
+#define AFTER_TRIGGER_FDW_FETCH 0x20000000
+#define AFTER_TRIGGER_1CTID 0x10000000
+#define AFTER_TRIGGER_2CTID 0x30000000
#define AFTER_TRIGGER_CP_UPDATE 0x08000000
-#define AFTER_TRIGGER_TUP_BITS 0xC8000000
-
+#define AFTER_TRIGGER_TUP_BITS 0x38000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
typedef struct AfterTriggerSharedData
@@ -3598,19 +3602,19 @@ typedef struct AfterTriggerEventData
/*
* During a cross-partition update of a partitioned table, we also store
- * the OIDs of source and destination partitions that are needed to
- * fetch the old (ctid1) and the new tuple (ctid2) from, respectively.
+ * the OIDs of source and destination partitions that are needed to fetch
+ * the old (ctid1) and the new tuple (ctid2) from, respectively.
*/
- Oid ate_src_part;
- Oid ate_dst_part;
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
typedef struct AfterTriggerEventDataNoOids
{
- TriggerFlags ate_flags; /* status bits and offset to shared data */
- ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
- ItemPointerData ate_ctid2; /* new updated tuple */
+ TriggerFlags ate_flags;
+ ItemPointerData ate_ctid1;
+ ItemPointerData ate_ctid2;
} AfterTriggerEventDataNoOids;
/* AfterTriggerEventData, minus ate_ctid2 */
@@ -4265,9 +4269,8 @@ AfterTriggerExecute(EState *estate,
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
/*
- * Store the tuple fetched from the source partition into
- * the target (root partitioned) table slot, converting if
- * needed.
+ * Store the tuple fetched from the source partition into the
+ * target (root partitioned) table slot, converting if needed.
*/
if (src_relInfo != relInfo)
{
@@ -4294,10 +4297,8 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID ||
- (event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_CP_UPDATE) &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_CP_UPDATE)) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
@@ -4559,7 +4560,8 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_firing_id == firing_id)
{
ResultRelInfo *src_rInfo,
- *dst_rInfo;
+ *dst_rInfo;
+
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
@@ -4596,17 +4598,17 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
* Look up source and destination partition result rels of a
* cross-partition update event.
*/
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS ) ==
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
AFTER_TRIGGER_CP_UPDATE)
{
Assert(OidIsValid(event->ate_src_part) &&
OidIsValid(event->ate_dst_part));
src_rInfo = ExecGetTriggerResultRel(estate,
- event->ate_src_part,
- rInfo);
+ event->ate_src_part,
+ rInfo);
dst_rInfo = ExecGetTriggerResultRel(estate,
- event->ate_dst_part,
- rInfo);
+ event->ate_dst_part,
+ rInfo);
}
else
src_rInfo = dst_rInfo = rInfo;
@@ -5824,7 +5826,9 @@ AfterTriggerPendingOnRel(Oid relid)
* as DELETE on the source partition followed by INSERT into the destination
* partition. Specifically, firing DELETE triggers would lead to the wrong
* foreign key action to be enforced considering that the original command is
- * UPDATE.
+ * UPDATE; in this case, this function is called with relinfo as the
+ * partitioned table, and src_partinfo and dst_partinfo referring to the
+ * source and target leaf partitions, respectively.
* ----------
*/
static void
@@ -5838,16 +5842,11 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
TransitionCaptureState *transition_capture)
{
Relation rel = relinfo->ri_RelationDesc;
- Relation rootRel = relinfo->ri_RootResultRelInfo ?
- relinfo->ri_RootResultRelInfo->ri_RelationDesc: NULL;
- bool maybe_crosspart_update =
- (row_trigger && mtstate && mtstate->operation == CMD_UPDATE &&
- (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- (rootRel && rootRel->rd_rel->relkind ==
- RELKIND_PARTITIONED_TABLE)));
+ Relation rootRel;
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
+ bool maybe_crosspart_update;
char relkind = rel->rd_rel->relkind;
int tgtype_event;
int tgtype_level;
@@ -5953,11 +5952,18 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
/*
* We normally don't see partitioned tables here for row level triggers
- * except in the special case of a cross-partitioned update. In that
- * case, nodeModifyTable.c: ExecCrossPartitionUpdateForeignKey() calls to
+ * except in the special case of a cross-partition update. In that case,
+ * nodeModifyTable.c:ExecCrossPartitionUpdateForeignKey() calls here to
* queue an update event on the root target partitioned table, also
* passing the source and destination partitions and their tuples.
*/
+ rootRel = relinfo->ri_RootResultRelInfo ?
+ relinfo->ri_RootResultRelInfo->ri_RelationDesc : NULL;
+ maybe_crosspart_update =
+ (row_trigger && mtstate &&
+ mtstate->operation == CMD_UPDATE &&
+ (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ (rootRel && rootRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)));
Assert(!row_trigger ||
rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
(maybe_crosspart_update &&
@@ -6024,6 +6030,7 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+
/*
* Also remember the OIDs of partitions to fetch these tuples
* out of later in AfterTriggerExecute().
@@ -6031,8 +6038,10 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
Assert(src_partinfo != NULL && dst_partinfo != NULL);
- new_event.ate_src_part = RelationGetRelid(src_partinfo->ri_RelationDesc);
- new_event.ate_dst_part = RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ new_event.ate_src_part =
+ RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part =
+ RelationGetRelid(dst_partinfo->ri_RelationDesc);
}
}
else
@@ -6058,11 +6067,19 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
break;
}
+ /* Determine flags */
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
- new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- (relkind == RELKIND_PARTITIONED_TABLE ? AFTER_TRIGGER_CP_UPDATE :
- AFTER_TRIGGER_2CTID) :
- AFTER_TRIGGER_1CTID;
+ {
+ if (row_trigger && event == TRIGGER_EVENT_UPDATE)
+ {
+ if (relkind == RELKIND_PARTITIONED_TABLE)
+ new_event.ate_flags = AFTER_TRIGGER_CP_UPDATE;
+ else
+ new_event.ate_flags = AFTER_TRIGGER_2CTID;
+ }
+ else
+ new_event.ate_flags = AFTER_TRIGGER_1CTID;
+ }
/* else, we'll initialize ate_flags for each trigger */
@@ -6070,8 +6087,8 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
/*
* Must convert/copy the source and destination partition tuples into the
- * root partitioned table's format/slot, because the processing in the loop
- * below expects both oldslot and newslot tuples to be in that form.
+ * root partitioned table's format/slot, because the processing in the
+ * loop below expects both oldslot and newslot tuples to be in that form.
*/
if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
{
@@ -6081,18 +6098,18 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
rootslot = ExecGetTriggerOldSlot(estate, relinfo);
map = ExecGetChildToRootMap(src_partinfo);
if (map)
- oldslot = execute_attr_map_slot(map->attrMap,
- oldslot,
- rootslot);
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
else
oldslot = ExecCopySlot(rootslot, oldslot);
rootslot = ExecGetTriggerNewSlot(estate, relinfo);
map = ExecGetChildToRootMap(dst_partinfo);
if (map)
- newslot = execute_attr_map_slot(map->attrMap,
- newslot,
- rootslot);
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
else
newslot = ExecCopySlot(rootslot, newslot);
}
@@ -6125,23 +6142,24 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
/*
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
- * tell by inspection that the FK constraint will still pass.
- * There are also some cases during cross-partition updates of a
- * partitioned table where queuing the event can be skipped.
+ * tell by inspection that the FK constraint will still pass. There
+ * are also some cases during cross-partition updates of a partitioned
+ * table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+
/*
* For cross-partitioned updates of partitioned PK table,
* skip the event fired by the component delete on the
* source leaf partition unless the constraint originates
- * in the partition itself (!tgisclone), because the update
- * event that will be fired on the root (partitioned)
- * target table will be used to perform the necessary
- * foreign key enforcement action.
+ * in the partition itself (!tgisclone), because the
+ * update event that will be fired on the root
+ * (partitioned) target table will be used to perform the
+ * necessary foreign key enforcement action.
*/
if (maybe_crosspart_update &&
TRIGGER_FIRED_BY_DELETE(event) &&
@@ -6158,6 +6176,7 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
break;
case RI_TRIGGER_FK:
+
/*
* Update on trigger's FK table. We can skip the update
* event fired on a partitioned table during a
@@ -6179,12 +6198,14 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
break;
case RI_TRIGGER_NONE:
+
/*
* Not an FK trigger. No need to queue the update event
- * fired during a cross-partitioned update of a partitioned
- * table, because the same row trigger must be present in
- * the leaf partition(s) that are affected as part of this
- * update and the events fired on them are queued instead.
+ * fired during a cross-partitioned update of a
+ * partitioned table, because the same row trigger must be
+ * present in the leaf partition(s) that are affected as
+ * part of this update and the events fired on them are
+ * queued instead.
*/
if (row_trigger &&
rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index b6a2d4f708..880b135058 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1448,7 +1448,7 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
- ListCell *lc;
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
foreach(lc, resultRelInfo->ri_ancestorResultRels)
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 7d32630030..6a16d0e673 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -585,6 +585,9 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
* to access "junk" columns that are not going to be stored.
*
* Returns RETURNING result if any, otherwise NULL.
+ * *inserted_tuple is the tuple that's effectively inserted;
+ * *inserted_destrel is the relation where it was inserted.
+ * These are only set on success. FIXME -- see what happens on the "do nothing" cases.
*
* This may change the currently active tuple conversion map in
* mtstate->mt_transition_capture, so the callers must take care to
@@ -1433,7 +1436,7 @@ ldelete:;
* for the caller.
*
* False is returned if the tuple we're trying to move is found to have been
- * concurrently updated. In that case, the caller must to check if the
+ * concurrently updated. In that case, the caller must check if the
* updated tuple that's returned in *retry_slot still needs to be re-routed,
* and call this function again or perform a regular update accordingly.
*/
@@ -1598,16 +1601,16 @@ GetAncestorResultRels(ResultRelInfo *resultRelInfo)
if (!partRel->rd_rel->relispartition)
elog(ERROR, "cannot find ancestors of a non-partition result relation");
Assert(rootRelInfo != NULL);
- rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
if (resultRelInfo->ri_ancestorResultRels == NIL)
{
- ListCell *lc;
- List *oids = get_partition_ancestors(RelationGetRelid(partRel));
- List *ancResultRels = NIL;
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
foreach(lc, oids)
{
- Oid ancOid = lfirst_oid(lc);
+ Oid ancOid = lfirst_oid(lc);
Relation ancRel;
ResultRelInfo *rInfo;
@@ -1634,9 +1637,9 @@ GetAncestorResultRels(ResultRelInfo *resultRelInfo)
}
/*
- * Queues up an update event using the target root partitioned table's trigger
- * to check that a cross-partition update hasn't broken any foreign keys
- * pointing into it.
+ * Queues up an update event using the target root partitioned table's
+ * trigger to check that a cross-partition update hasn't broken any foreign
+ * keys pointing into it.
*/
static void
ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
@@ -1647,9 +1650,9 @@ ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
ModifyTableState *mtstate,
EState *estate)
{
- ListCell *lc;
+ ListCell *lc;
ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
- List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
/*
* For any foreign keys that point directly into a non-root ancestors of
@@ -1662,18 +1665,18 @@ ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
{
ResultRelInfo *rInfo = lfirst(lc);
TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
- bool has_noncloned_fkey = false;
+ bool has_noncloned_fkey = false;
if (rInfo == rootRelInfo)
break;
if (trigdesc && trigdesc->trig_update_after_row)
{
- int i;
+ int i;
for (i = 0; i < trigdesc->numtriggers; i++)
{
- Trigger *trig = &trigdesc->triggers[i];
+ Trigger *trig = &trigdesc->triggers[i];
if (!trig->tgisclone &&
RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
@@ -1692,7 +1695,7 @@ ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
RelationGetRelationName(rInfo->ri_RelationDesc),
RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
errhint("Consider defining the foreign key on \"%s\".",
- RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
}
/* Perform the root table's triggers. */
@@ -1903,13 +1906,13 @@ lreplace:;
/*
* If the partitioned table being updated is referenced in foreign
* keys, queue up trigger events to check that none of them were
- * violated. No special treatment is needed in non-cross-partition
- * update situations, because the leaf partition's AR update
- * triggers will take care of that. During cross-partition
- * updates implemented as delete on the source partition followed
- * by insert on the destination partition, AR update triggers of
- * the root table (that is, the table mentioned in the query) must
- * be fired.
+ * violated. No special treatment is needed in
+ * non-cross-partition update situations, because the leaf
+ * partition's AR update triggers will take care of that. During
+ * cross-partition updates implemented as delete on the source
+ * partition followed by insert on the destination partition,
+ * AR-UPDATE triggers of the root table (that is, the table
+ * mentioned in the query) must be fired.
*
* NULL insert_destrel means that the move failed to occur, that
* is, the update failed, so no need to anything in that case.
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index dcff6aeca5..03c587730c 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -204,7 +204,7 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
ResultRelInfo *partition_root_rri,
int instrument_options);
extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
- ResultRelInfo *rootRelInfo);
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
--
2.30.2
On Wed, Jan 19, 2022 at 7:29 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Jan-18, Alvaro Herrera wrote:
On 2022-Jan-18, Amit Langote wrote:
Would you like me to update the patch with the above renumbering or
are you working on a new version yourself?I have a few very minor changes apart from that. Let me see if I can
get this pushed soon; if I'm unable to (there are parts I don't fully
grok yet), I'll post what I have.Here's v13, a series with your patch as 0001 and a few changes from me;
the bulk is just pgindent, and there are a few stylistic changes and an
unrelated typo fix and I added a couple of comments to your new code.
Thank you.
I don't like the API change to ExecInsert(). You're adding two output
arguments:
- the tuple being inserted. But surely you have this already, because
it's in the 'slot' argument you passed in. ExecInsert is even careful to
set the ->tts_tableOid argument there. So ExecInsert's caller doesn't
need to receive the inserted tuple as an argument, it can just read
'slot'.
Hmm, ExecInsert()'s input slot belongs to either the source partition
or the "root" target relation, the latter due to the following stanza
in ExecCrossPartitionUpdate():
/*
* resultRelInfo is one of the per-relation resultRelInfos. So we should
* convert the tuple into root's tuple descriptor if needed, since
* ExecInsert() starts the search from root.
*/
tupconv_map = ExecGetChildToRootMap(resultRelInfo);
if (tupconv_map != NULL)
slot = execute_attr_map_slot(tupconv_map->attrMap,
slot,
mtstate->mt_root_tuple_slot);
Though the slot whose tuple ExecInsert() ultimately inserts may be
destination partition's ri_PartitionTupleSlot due to the following
stanza in it:
/*
* If the input result relation is a partitioned table, find the leaf
* partition to insert the tuple into.
*/
if (proute)
{
ResultRelInfo *partRelInfo;
slot = ExecPrepareTupleRouting(mtstate, estate, proute,
resultRelInfo, slot,
&partRelInfo);
resultRelInfo = partRelInfo;
}
It's not great that ExecInsert()'s existing header comment doesn't
mention that the slot whose tuple is actually inserted may not be the
slot it receives from the caller :-(.
- the relation to which the tuple was inserted. Can't this be obtained
by looking at slot->tts_tableOid? We should be able to use
ExecLookupResultRelByOid() to obtain it, no? (I suppose you may claim
that this is wasteful, but I think this is not a common case anyway and
it's worth keeping ExecInsert()'s API clean for the sake of the 99.99%
of its other calls).
ExecLookupResultRelByOid() is only useful when *all* relevant leaf
partitions are present in the ModifyTableState.resultRelInfo array
(due to being present in ModifyTable.resultRelations). Leaf
partitions that are only initialized by tuple routing (such as
destination partitions of cross-partition updates) are only present in
ModifyTableState.mt_partition_tuple_routing.partitions[] that are not
discoverable by ExecLookupResultRelByOid().
I think the argument definition of ExecCrossPartitionUpdateForeignKey is
a bit messy. I propose to move mtstate,estate as two first arguments;
IIRC the whole executor does it that way.
Okay, done.
AfterTriggerSaveEvent determines maybe_crosspart_update (by looking at
mtstate->operation -- why doesn't it look at 'event' instead?) and later
it determines new_event.ate_flags. Why can't it use
maybe_crosspart_update to simplify part of that? Or maybe the other way
around, not sure. It looks like something could be made simpler there.
Actually, I remember disliking maybe_crosspart_update for sounding a
bit fuzzy and also having to add mtstate to a bunch of trigger.c
interface functions only to calculate that.
I now wonder if passing an is_crosspart_update through
ExecAR{Update|Delete}Triggers() would not be better. Both
ExecDelete() and ExecCrossPartitionUpdateForeignKey() know for sure if
their ExecAR{Update|Delete}Triggers() invocation is for a
cross-partition update, so better to just pass that information down
to AfterTriggerSaveEvent() than pass 'mtstate' and have the latter
reverse-engineer only a fuzzy guess of whether that's the case.
I like that interface better and have implemented it in the updated version.
I've also merged your changes and made some of my own as mentioned
above to end up with the attached v14. I'm also attaching a delta
patch over v13 (0001+0002) to easily see the changes I made to get
v14.
BTW, your tweaks patch added this:
+ * *inserted_tuple is the tuple that's effectively inserted;
+ * *inserted_destrel is the relation where it was inserted.
+ * These are only set on success. FIXME -- see what happens on
the "do nothing" cases.
If by "do nothing cases" you mean INSERT ON CONFLICT ... DO NOTHING,
then I don't think it matters, because the caller in that case would
be ExecModifyTable() which doesn't care about inserted_tuple and
inserted_destrel.
Overall, the idea embodied in the patch looks sensible to me.
Thanks again for taking time to review this.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
delta-over-v13.diffapplication/octet-stream; name=delta-over-v13.diffDownload
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index e6aa36a9d8..c2a643e5a8 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,15 +94,14 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate,
- ModifyTableState *mtstate,
- ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
ResultRelInfo *src_partinfo,
ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
@@ -2462,10 +2461,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, NULL, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_INSERT,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
bool
@@ -2553,12 +2552,12 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, NULL, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -2680,10 +2679,10 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, NULL, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_DELETE,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
/*
@@ -2778,12 +2777,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
return result;
}
+/*
+ * Note: is_crosspart_update must be true if the DELETE is being performed
+ * as part of a cross-partition update.
+ */
void
-ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -2804,11 +2808,11 @@ ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, mtstate, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -2927,12 +2931,12 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, NULL, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -3068,24 +3072,25 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
/*
- * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
- * destination partitions, respectively, of a cross-partition update of the
- * root partitioned table mentioned in the query, given by 'relinfo'.
+ * Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source
+ * and destination partitions, respectively, of a cross-partition update of
+ * the root partitioned table mentioned in the query, given by 'relinfo'.
* 'tupleid' in that case refers to the ctid of the "old" tuple in the source
* partition, and 'newslot' contains the "new" tuple in the destination
* partition. This interface allows to support the requirements of
- * ExecCrossPartitionUpdateForeignKey().
+ * ExecCrossPartitionUpdateForeignKey(); is_crosspart_update must be true in
+ * that case.
*/
void
-ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
- ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
ResultRelInfo *src_partinfo,
ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -3103,6 +3108,9 @@ ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
TupleTableSlot *oldslot;
ResultRelInfo *tupsrc;
+ Assert((src_partinfo != NULL && dst_partinfo != NULL) ||
+ !is_crosspart_update);
+
tupsrc = src_partinfo ? src_partinfo : relinfo;
oldslot = ExecGetTriggerOldSlot(estate, tupsrc);
@@ -3119,12 +3127,14 @@ ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ AfterTriggerSaveEvent(estate, relinfo,
src_partinfo, dst_partinfo,
TRIGGER_EVENT_UPDATE,
- true, oldslot, newslot, recheckIndexes,
+ true,
+ oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -3247,10 +3257,11 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, NULL, relinfo,
+ AfterTriggerSaveEvent(estate, relinfo,
NULL, NULL,
TRIGGER_EVENT_TRUNCATE,
- false, NULL, NULL, NIL, NULL, NULL);
+ false, NULL, NULL, NIL, NULL, NULL,
+ false);
}
@@ -5829,24 +5840,26 @@ AfterTriggerPendingOnRel(Oid relid)
* UPDATE; in this case, this function is called with relinfo as the
* partitioned table, and src_partinfo and dst_partinfo referring to the
* source and target leaf partitions, respectively.
+ *
+ * is_crosspart_update is true either when a DELETE event is fired on the
+ * source partition (which is to be ignored) or an UPDATE event is fired on
+ * the root partitioned table.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
- ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
ResultRelInfo *src_partinfo,
ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
Relation rel = relinfo->ri_RelationDesc;
- Relation rootRel;
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
- bool maybe_crosspart_update;
char relkind = rel->rd_rel->relkind;
int tgtype_event;
int tgtype_level;
@@ -5957,16 +5970,9 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
* queue an update event on the root target partitioned table, also
* passing the source and destination partitions and their tuples.
*/
- rootRel = relinfo->ri_RootResultRelInfo ?
- relinfo->ri_RootResultRelInfo->ri_RelationDesc : NULL;
- maybe_crosspart_update =
- (row_trigger && mtstate &&
- mtstate->operation == CMD_UPDATE &&
- (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- (rootRel && rootRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)));
Assert(!row_trigger ||
rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
- (maybe_crosspart_update &&
+ (is_crosspart_update &&
TRIGGER_FIRED_BY_UPDATE(event) &&
src_partinfo != NULL && dst_partinfo != NULL));
@@ -6161,7 +6167,7 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
* (partitioned) target table will be used to perform the
* necessary foreign key enforcement action.
*/
- if (maybe_crosspart_update &&
+ if (is_crosspart_update &&
TRIGGER_FIRED_BY_DELETE(event) &&
trigger->tgisclone)
continue;
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index e2a338ba33..f978e28ba9 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,10 +516,10 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ ExecARUpdateTriggers(estate, resultRelInfo,
NULL, NULL,
tid, NULL, slot,
- recheckIndexes, NULL);
+ recheckIndexes, NULL, false);
list_free(recheckIndexes);
}
@@ -557,8 +557,8 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, NULL, resultRelInfo,
- tid, NULL, NULL);
+ ExecARDeleteTriggers(estate, resultRelInfo,
+ tid, NULL, NULL, false);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6a16d0e673..204126a29f 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -962,13 +962,14 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ ExecARUpdateTriggers(estate, resultRelInfo,
NULL, NULL,
NULL,
NULL,
slot,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1359,13 +1360,14 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ ExecARUpdateTriggers(estate, resultRelInfo,
NULL, NULL,
tupleid,
oldtuple,
NULL,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1375,8 +1377,8 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
- ar_delete_trig_tcs);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ar_delete_trig_tcs, changingPart);
/* Process RETURNING if present and if requested */
if (processReturning && resultRelInfo->ri_projectReturning)
@@ -1642,13 +1644,13 @@ GetAncestorResultRels(ResultRelInfo *resultRelInfo)
* keys pointing into it.
*/
static void
-ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ExecCrossPartitionUpdateForeignKey(ModifyTableState *mtstate,
+ EState *estate,
+ ResultRelInfo *sourcePartInfo,
ResultRelInfo *destPartInfo,
ItemPointer tupleid,
TupleTableSlot *oldslot,
- TupleTableSlot *newslot,
- ModifyTableState *mtstate,
- EState *estate)
+ TupleTableSlot *newslot)
{
ListCell *lc;
ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
@@ -1699,10 +1701,9 @@ ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
}
/* Perform the root table's triggers. */
- ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
- sourcePartInfo, destPartInfo,
- tupleid, NULL,
- newslot, NIL, NULL);
+ ExecARUpdateTriggers(estate,
+ rootRelInfo, sourcePartInfo, destPartInfo,
+ tupleid, NULL, newslot, NIL, NULL, true);
}
/* ----------------------------------------------------------------
@@ -1920,11 +1921,11 @@ lreplace:;
if (insert_destrel &&
resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_update_after_row)
- ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ ExecCrossPartitionUpdateForeignKey(mtstate, estate,
+ resultRelInfo,
insert_destrel,
tupleid, oldslot,
- inserted_tuple,
- mtstate, estate);
+ inserted_tuple);
return returning_slot;
}
@@ -2105,14 +2106,15 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ ExecARUpdateTriggers(estate, resultRelInfo,
NULL, NULL,
tupleid, oldtuple,
slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
list_free(recheckIndexes);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 1ba3a54499..66bf6c16e3 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,11 +211,11 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
- ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -231,7 +231,6 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
- ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ResultRelInfo *src_partinfo,
ResultRelInfo *dst_partinfo,
@@ -239,7 +238,8 @@ extern void ExecARUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 6c7eef1e54..37ad2b7663 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -530,7 +530,10 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
- /* Used during cross-partition updates on partitioned tables. */
+ /*
+ * Used when a leaf partition is involved in a cross-partition update of
+ * one of its ancestors; see ExecCrossPartitionUpdateForeignKey().
+ */
List *ri_ancestorResultRels;
} ResultRelInfo;
v14-0001-Enforce-foreign-key-correctly-during-cross-parti.patchapplication/octet-stream; name=v14-0001-Enforce-foreign-key-correctly-during-cross-parti.patchDownload
From 0bfd0520188aeecd73bf02594e0a026167b04364 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH v14] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 389 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 198 ++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 8 +-
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 6 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++-
src/test/regress/sql/foreign_key.sql | 135 +++++++-
11 files changed, 900 insertions(+), 80 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 1a9c1ac290..c2a643e5a8 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -95,10 +95,13 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
Instrumentation *instr,
MemoryContext per_tuple_context);
static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
@@ -2458,8 +2461,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_INSERT,
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
bool
@@ -2547,10 +2552,12 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -2672,8 +2679,10 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_DELETE,
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
/*
@@ -2768,11 +2777,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
return result;
}
+/*
+ * Note: is_crosspart_update must be true if the DELETE is being performed
+ * as part of a cross-partition update.
+ */
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -2793,9 +2808,11 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -2914,10 +2931,12 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -3052,13 +3071,26 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source
+ * and destination partitions, respectively, of a cross-partition update of
+ * the root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey(); is_crosspart_update must be true in
+ * that case.
+ */
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -3073,12 +3105,19 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot;
+ ResultRelInfo *tupsrc;
+
+ Assert((src_partinfo != NULL && dst_partinfo != NULL) ||
+ !is_crosspart_update);
+
+ tupsrc = src_partinfo ? src_partinfo : relinfo;
+ oldslot = ExecGetTriggerOldSlot(estate, tupsrc);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ tupsrc,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,10 +3127,14 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
- true, oldslot, newslot, recheckIndexes,
+ AfterTriggerSaveEvent(estate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
+ true,
+ oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -3214,8 +3257,11 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
- false, NULL, NULL, NIL, NULL, NULL);
+ AfterTriggerSaveEvent(estate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
+ false, NULL, NULL, NIL, NULL, NULL,
+ false);
}
@@ -3496,9 +3542,9 @@ typedef SetConstraintStateData *SetConstraintState;
* Per-trigger-event data
*
* The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS
- * status bits and up to two tuple CTIDs. Each event record also has an
- * associated AfterTriggerSharedData that is shared across all instances of
- * similar events within a "chunk".
+ * status bits, up to two tuple CTIDs, and optionally two OIDs of partitions
+ * Each event record also has an associated AfterTriggerSharedData that is
+ * shared across all instances of similar events within a "chunk".
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
@@ -3509,6 +3555,10 @@ typedef SetConstraintStateData *SetConstraintState;
* tuple(s). This permits storing tuples once regardless of the number of
* row-level triggers on a foreign table.
*
+ * When updates move tuples in partitioned tables to different partitions,
+ * the OIDs of both partitions are stored too, so that the tuples can be
+ * fetched.
+ *
* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
* the right order. To ensure that, we forbid deferrable (constraint)
@@ -3531,16 +3581,16 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
-#define AFTER_TRIGGER_DONE 0x10000000
-#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_DONE 0x80000000
+#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
/* bits describing the size and tuple sources of this event */
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
-#define AFTER_TRIGGER_1CTID 0x40000000
-#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
-
+#define AFTER_TRIGGER_FDW_FETCH 0x20000000
+#define AFTER_TRIGGER_1CTID 0x10000000
+#define AFTER_TRIGGER_2CTID 0x30000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0x38000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
typedef struct AfterTriggerSharedData
@@ -3560,8 +3610,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to fetch
+ * the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags;
+ ItemPointerData ate_ctid1;
+ ItemPointerData ate_ctid2;
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3576,11 +3642,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3830,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4086,8 +4156,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4101,6 +4179,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4108,6 +4188,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4188,12 +4270,35 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into the
+ * target (root partitioned) table slot, converting if needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4203,16 +4308,40 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_CP_UPDATE)) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4441,13 +4570,17 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
+
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4472,12 +4605,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5672,14 +5826,35 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE; in this case, this function is called with relinfo as the
+ * partitioned table, and src_partinfo and dst_partinfo referring to the
+ * source and target leaf partitions, respectively.
+ *
+ * is_crosspart_update is true either when a DELETE event is fired on the
+ * source partition (which is to be ignored) or an UPDATE event is fired on
+ * the root partitioned table.
* ----------
*/
static void
AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
Relation rel = relinfo->ri_RelationDesc;
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5788,6 +5963,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partition update. In that case,
+ * nodeModifyTable.c:ExecCrossPartitionUpdateForeignKey() calls here to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (is_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5848,6 +6036,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part =
+ RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part =
+ RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5872,13 +6073,53 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
+ /* Determine flags */
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
- new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ {
+ if (row_trigger && event == TRIGGER_EVENT_UPDATE)
+ {
+ if (relkind == RELKIND_PARTITIONED_TABLE)
+ new_event.ate_flags = AFTER_TRIGGER_CP_UPDATE;
+ else
+ new_event.ate_flags = AFTER_TRIGGER_2CTID;
+ }
+ else
+ new_event.ate_flags = AFTER_TRIGGER_1CTID;
+ }
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the
+ * loop below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5907,13 +6148,30 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
/*
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
- * tell by inspection that the FK constraint will still pass.
+ * tell by inspection that the FK constraint will still pass. There
+ * are also some cases during cross-partition updates of a partitioned
+ * table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the
+ * update event that will be fired on the root
+ * (partitioned) target table will be used to perform the
+ * necessary foreign key enforcement action.
+ */
+ if (is_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5924,8 +6182,20 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5934,7 +6204,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a
+ * partitioned table, because the same row trigger must be
+ * present in the leaf partition(s) that are affected as
+ * part of this update and the events fired on them are
+ * queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 549d9eb696..880b135058 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 313c87398b..f978e28ba9 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -517,8 +517,9 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
- recheckIndexes, NULL);
+ recheckIndexes, NULL, false);
list_free(recheckIndexes);
}
@@ -557,7 +558,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo,
- tid, NULL, NULL);
+ tid, NULL, NULL, false);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 5ec699a9bd..204126a29f 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -584,6 +585,9 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
* to access "junk" columns that are not going to be stored.
*
* Returns RETURNING result if any, otherwise NULL.
+ * *inserted_tuple is the tuple that's effectively inserted;
+ * *inserted_destrel is the relation where it was inserted.
+ * These are only set on success. FIXME -- see what happens on the "do nothing" cases.
*
* This may change the currently active tuple conversion map in
* mtstate->mt_transition_capture, so the callers must take care to
@@ -596,7 +600,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,11 +962,14 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -994,6 +1003,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1347,11 +1361,13 @@ ldelete:;
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1362,7 +1378,7 @@ ldelete:;
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
- ar_delete_trig_tcs);
+ ar_delete_trig_tcs, changingPart);
/* Process RETURNING if present and if requested */
if (processReturning && resultRelInfo->ri_projectReturning)
@@ -1422,7 +1438,7 @@ ldelete:;
* for the caller.
*
* False is returned if the tuple we're trying to move is found to have been
- * concurrently updated. In that case, the caller must to check if the
+ * concurrently updated. In that case, the caller must check if the
* updated tuple that's returned in *retry_slot still needs to be re-routed,
* and call this function again or perform a regular update accordingly.
*/
@@ -1433,7 +1449,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1574,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1589,123 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's
+ * trigger to check that a cross-partition update hasn't broken any foreign
+ * keys pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ModifyTableState *mtstate,
+ EState *estate,
+ ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate,
+ rootRelInfo, sourcePartInfo, destPartInfo,
+ tupleid, NULL, newslot, NIL, NULL, true);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1878,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1895,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in
+ * non-cross-partition update situations, because the leaf
+ * partition's AR update triggers will take care of that. During
+ * cross-partition updates implemented as delete on the source
+ * partition followed by insert on the destination partition,
+ * AR-UPDATE triggers of the root table (that is, the table
+ * mentioned in the query) must be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(mtstate, estate,
+ resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple);
+
+ return returning_slot;
}
/*
@@ -1942,11 +2106,15 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
list_free(recheckIndexes);
@@ -2559,7 +2727,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index c95cd32402..01d4c22cfc 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index e1271420e5..66bf6c16e3 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -214,7 +214,8 @@ extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -231,11 +232,14 @@ extern bool ExecBRUpdateTriggers(EState *estate,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 344399f6a8..03c587730c 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4ea8735dd8..37ad2b7663 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -529,6 +529,12 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /*
+ * Used when a leaf partition is involved in a cross-partition update of
+ * one of its ancestors; see ExecCrossPartitionUpdateForeignKey().
+ */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
On Wed, Jan 19, 2022 at 4:13 PM Amit Langote <amitlangote09@gmail.com> wrote:
On Wed, Jan 19, 2022 at 7:29 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Jan-18, Alvaro Herrera wrote:
On 2022-Jan-18, Amit Langote wrote:
Would you like me to update the patch with the above renumbering or
are you working on a new version yourself?I have a few very minor changes apart from that. Let me see if I can
get this pushed soon; if I'm unable to (there are parts I don't fully
grok yet), I'll post what I have.Here's v13, a series with your patch as 0001 and a few changes from me;
the bulk is just pgindent, and there are a few stylistic changes and an
unrelated typo fix and I added a couple of comments to your new code.Thank you.
I don't like the API change to ExecInsert(). You're adding two output
arguments:
- the tuple being inserted. But surely you have this already, because
it's in the 'slot' argument you passed in. ExecInsert is even careful to
set the ->tts_tableOid argument there. So ExecInsert's caller doesn't
need to receive the inserted tuple as an argument, it can just read
'slot'.Hmm, ExecInsert()'s input slot belongs to either the source partition
or the "root" target relation, the latter due to the following stanza
in ExecCrossPartitionUpdate():/*
* resultRelInfo is one of the per-relation resultRelInfos. So we should
* convert the tuple into root's tuple descriptor if needed, since
* ExecInsert() starts the search from root.
*/
tupconv_map = ExecGetChildToRootMap(resultRelInfo);
if (tupconv_map != NULL)
slot = execute_attr_map_slot(tupconv_map->attrMap,
slot,
mtstate->mt_root_tuple_slot);Though the slot whose tuple ExecInsert() ultimately inserts may be
destination partition's ri_PartitionTupleSlot due to the following
stanza in it:/*
* If the input result relation is a partitioned table, find the leaf
* partition to insert the tuple into.
*/
if (proute)
{
ResultRelInfo *partRelInfo;slot = ExecPrepareTupleRouting(mtstate, estate, proute,
resultRelInfo, slot,
&partRelInfo);
resultRelInfo = partRelInfo;
}It's not great that ExecInsert()'s existing header comment doesn't
mention that the slot whose tuple is actually inserted may not be the
slot it receives from the caller :-(.- the relation to which the tuple was inserted. Can't this be obtained
by looking at slot->tts_tableOid? We should be able to use
ExecLookupResultRelByOid() to obtain it, no? (I suppose you may claim
that this is wasteful, but I think this is not a common case anyway and
it's worth keeping ExecInsert()'s API clean for the sake of the 99.99%
of its other calls).ExecLookupResultRelByOid() is only useful when *all* relevant leaf
partitions are present in the ModifyTableState.resultRelInfo array
(due to being present in ModifyTable.resultRelations). Leaf
partitions that are only initialized by tuple routing (such as
destination partitions of cross-partition updates) are only present in
ModifyTableState.mt_partition_tuple_routing.partitions[] that are not
discoverable by ExecLookupResultRelByOid().I think the argument definition of ExecCrossPartitionUpdateForeignKey is
a bit messy. I propose to move mtstate,estate as two first arguments;
IIRC the whole executor does it that way.Okay, done.
AfterTriggerSaveEvent determines maybe_crosspart_update (by looking at
mtstate->operation -- why doesn't it look at 'event' instead?) and later
it determines new_event.ate_flags. Why can't it use
maybe_crosspart_update to simplify part of that? Or maybe the other way
around, not sure. It looks like something could be made simpler there.Actually, I remember disliking maybe_crosspart_update for sounding a
bit fuzzy and also having to add mtstate to a bunch of trigger.c
interface functions only to calculate that.I now wonder if passing an is_crosspart_update through
ExecAR{Update|Delete}Triggers() would not be better. Both
ExecDelete() and ExecCrossPartitionUpdateForeignKey() know for sure if
their ExecAR{Update|Delete}Triggers() invocation is for a
cross-partition update, so better to just pass that information down
to AfterTriggerSaveEvent() than pass 'mtstate' and have the latter
reverse-engineer only a fuzzy guess of whether that's the case.I like that interface better and have implemented it in the updated version.
I've also merged your changes and made some of my own as mentioned
above to end up with the attached v14. I'm also attaching a delta
patch over v13 (0001+0002) to easily see the changes I made to get
v14.
Oops, broke the cfbot's patch-applier again. Delta-diff reattached as txt.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
delta-over-v13.diff.txttext/plain; charset=US-ASCII; name=delta-over-v13.diff.txtDownload
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index e6aa36a9d8..c2a643e5a8 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -94,15 +94,14 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
FmgrInfo *finfo,
Instrumentation *instr,
MemoryContext per_tuple_context);
-static void AfterTriggerSaveEvent(EState *estate,
- ModifyTableState *mtstate,
- ResultRelInfo *relinfo,
+static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
ResultRelInfo *src_partinfo,
ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
@@ -2462,10 +2461,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, NULL, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_INSERT,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
bool
@@ -2553,12 +2552,12 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, NULL, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -2680,10 +2679,10 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, NULL, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_DELETE,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
/*
@@ -2778,12 +2777,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
return result;
}
+/*
+ * Note: is_crosspart_update must be true if the DELETE is being performed
+ * as part of a cross-partition update.
+ */
void
-ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
+ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -2804,11 +2808,11 @@ ExecARDeleteTriggers(EState *estate, ModifyTableState *mtstate,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, mtstate, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -2927,12 +2931,12 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, NULL, relinfo,
- NULL, NULL,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -3068,24 +3072,25 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
}
/*
- * 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source and
- * destination partitions, respectively, of a cross-partition update of the
- * root partitioned table mentioned in the query, given by 'relinfo'.
+ * Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source
+ * and destination partitions, respectively, of a cross-partition update of
+ * the root partitioned table mentioned in the query, given by 'relinfo'.
* 'tupleid' in that case refers to the ctid of the "old" tuple in the source
* partition, and 'newslot' contains the "new" tuple in the destination
* partition. This interface allows to support the requirements of
- * ExecCrossPartitionUpdateForeignKey().
+ * ExecCrossPartitionUpdateForeignKey(); is_crosspart_update must be true in
+ * that case.
*/
void
-ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
- ResultRelInfo *relinfo,
+ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
ResultRelInfo *src_partinfo,
ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -3103,6 +3108,9 @@ ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
TupleTableSlot *oldslot;
ResultRelInfo *tupsrc;
+ Assert((src_partinfo != NULL && dst_partinfo != NULL) ||
+ !is_crosspart_update);
+
tupsrc = src_partinfo ? src_partinfo : relinfo;
oldslot = ExecGetTriggerOldSlot(estate, tupsrc);
@@ -3119,12 +3127,14 @@ ExecARUpdateTriggers(EState *estate, ModifyTableState *mtstate,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, mtstate, relinfo,
+ AfterTriggerSaveEvent(estate, relinfo,
src_partinfo, dst_partinfo,
TRIGGER_EVENT_UPDATE,
- true, oldslot, newslot, recheckIndexes,
+ true,
+ oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -3247,10 +3257,11 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, NULL, relinfo,
+ AfterTriggerSaveEvent(estate, relinfo,
NULL, NULL,
TRIGGER_EVENT_TRUNCATE,
- false, NULL, NULL, NIL, NULL, NULL);
+ false, NULL, NULL, NIL, NULL, NULL,
+ false);
}
@@ -5829,24 +5840,26 @@ AfterTriggerPendingOnRel(Oid relid)
* UPDATE; in this case, this function is called with relinfo as the
* partitioned table, and src_partinfo and dst_partinfo referring to the
* source and target leaf partitions, respectively.
+ *
+ * is_crosspart_update is true either when a DELETE event is fired on the
+ * source partition (which is to be ignored) or an UPDATE event is fired on
+ * the root partitioned table.
* ----------
*/
static void
-AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
- ResultRelInfo *relinfo,
+AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
ResultRelInfo *src_partinfo,
ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
Relation rel = relinfo->ri_RelationDesc;
- Relation rootRel;
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
AfterTriggerEventData new_event;
AfterTriggerSharedData new_shared;
- bool maybe_crosspart_update;
char relkind = rel->rd_rel->relkind;
int tgtype_event;
int tgtype_level;
@@ -5957,16 +5970,9 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
* queue an update event on the root target partitioned table, also
* passing the source and destination partitions and their tuples.
*/
- rootRel = relinfo->ri_RootResultRelInfo ?
- relinfo->ri_RootResultRelInfo->ri_RelationDesc : NULL;
- maybe_crosspart_update =
- (row_trigger && mtstate &&
- mtstate->operation == CMD_UPDATE &&
- (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
- (rootRel && rootRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)));
Assert(!row_trigger ||
rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
- (maybe_crosspart_update &&
+ (is_crosspart_update &&
TRIGGER_FIRED_BY_UPDATE(event) &&
src_partinfo != NULL && dst_partinfo != NULL));
@@ -6161,7 +6167,7 @@ AfterTriggerSaveEvent(EState *estate, ModifyTableState *mtstate,
* (partitioned) target table will be used to perform the
* necessary foreign key enforcement action.
*/
- if (maybe_crosspart_update &&
+ if (is_crosspart_update &&
TRIGGER_FIRED_BY_DELETE(event) &&
trigger->tgisclone)
continue;
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index e2a338ba33..f978e28ba9 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -516,10 +516,10 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
NULL, NIL);
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, NULL, resultRelInfo,
+ ExecARUpdateTriggers(estate, resultRelInfo,
NULL, NULL,
tid, NULL, slot,
- recheckIndexes, NULL);
+ recheckIndexes, NULL, false);
list_free(recheckIndexes);
}
@@ -557,8 +557,8 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
simple_table_tuple_delete(rel, tid, estate->es_snapshot);
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, NULL, resultRelInfo,
- tid, NULL, NULL);
+ ExecARDeleteTriggers(estate, resultRelInfo,
+ tid, NULL, NULL, false);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6a16d0e673..204126a29f 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -962,13 +962,14 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ ExecARUpdateTriggers(estate, resultRelInfo,
NULL, NULL,
NULL,
NULL,
slot,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1359,13 +1360,14 @@ ldelete:;
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ ExecARUpdateTriggers(estate, resultRelInfo,
NULL, NULL,
tupleid,
oldtuple,
NULL,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1375,8 +1377,8 @@ ldelete:;
}
/* AFTER ROW DELETE Triggers */
- ExecARDeleteTriggers(estate, mtstate, resultRelInfo, tupleid, oldtuple,
- ar_delete_trig_tcs);
+ ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+ ar_delete_trig_tcs, changingPart);
/* Process RETURNING if present and if requested */
if (processReturning && resultRelInfo->ri_projectReturning)
@@ -1642,13 +1644,13 @@ GetAncestorResultRels(ResultRelInfo *resultRelInfo)
* keys pointing into it.
*/
static void
-ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
+ExecCrossPartitionUpdateForeignKey(ModifyTableState *mtstate,
+ EState *estate,
+ ResultRelInfo *sourcePartInfo,
ResultRelInfo *destPartInfo,
ItemPointer tupleid,
TupleTableSlot *oldslot,
- TupleTableSlot *newslot,
- ModifyTableState *mtstate,
- EState *estate)
+ TupleTableSlot *newslot)
{
ListCell *lc;
ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
@@ -1699,10 +1701,9 @@ ExecCrossPartitionUpdateForeignKey(ResultRelInfo *sourcePartInfo,
}
/* Perform the root table's triggers. */
- ExecARUpdateTriggers(estate, mtstate, rootRelInfo,
- sourcePartInfo, destPartInfo,
- tupleid, NULL,
- newslot, NIL, NULL);
+ ExecARUpdateTriggers(estate,
+ rootRelInfo, sourcePartInfo, destPartInfo,
+ tupleid, NULL, newslot, NIL, NULL, true);
}
/* ----------------------------------------------------------------
@@ -1920,11 +1921,11 @@ lreplace:;
if (insert_destrel &&
resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_update_after_row)
- ExecCrossPartitionUpdateForeignKey(resultRelInfo,
+ ExecCrossPartitionUpdateForeignKey(mtstate, estate,
+ resultRelInfo,
insert_destrel,
tupleid, oldslot,
- inserted_tuple,
- mtstate, estate);
+ inserted_tuple);
return returning_slot;
}
@@ -2105,14 +2106,15 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, mtstate, resultRelInfo,
+ ExecARUpdateTriggers(estate, resultRelInfo,
NULL, NULL,
tupleid, oldtuple,
slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
list_free(recheckIndexes);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 1ba3a54499..66bf6c16e3 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -211,11 +211,11 @@ extern bool ExecBRDeleteTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot);
extern void ExecARDeleteTriggers(EState *estate,
- ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -231,7 +231,6 @@ extern bool ExecBRUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
- ModifyTableState *mtstate,
ResultRelInfo *relinfo,
ResultRelInfo *src_partinfo,
ResultRelInfo *dst_partinfo,
@@ -239,7 +238,8 @@ extern void ExecARUpdateTriggers(EState *estate,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 6c7eef1e54..37ad2b7663 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -530,7 +530,10 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
- /* Used during cross-partition updates on partitioned tables. */
+ /*
+ * Used when a leaf partition is involved in a cross-partition update of
+ * one of its ancestors; see ExecCrossPartitionUpdateForeignKey().
+ */
List *ri_ancestorResultRels;
} ResultRelInfo;
v14-0001-Enforce-foreign-key-correctly-during-cross-parti.patchapplication/octet-stream; name=v14-0001-Enforce-foreign-key-correctly-during-cross-parti.patchDownload
From 0bfd0520188aeecd73bf02594e0a026167b04364 Mon Sep 17 00:00:00 2001
From: amitlan <amitlangote09@gmail.com>
Date: Mon, 11 Oct 2021 14:57:19 +0900
Subject: [PATCH v14] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 389 +++++++++++++++++++---
src/backend/executor/execMain.c | 19 +-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 198 ++++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 8 +-
src/include/executor/executor.h | 3 +-
src/include/nodes/execnodes.h | 6 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++-
src/test/regress/sql/foreign_key.sql | 135 +++++++-
11 files changed, 900 insertions(+), 80 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 1a9c1ac290..c2a643e5a8 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -95,10 +95,13 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
Instrumentation *instr,
MemoryContext per_tuple_context);
static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
@@ -2458,8 +2461,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_INSERT,
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
bool
@@ -2547,10 +2552,12 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -2672,8 +2679,10 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_DELETE,
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
/*
@@ -2768,11 +2777,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
return result;
}
+/*
+ * Note: is_crosspart_update must be true if the DELETE is being performed
+ * as part of a cross-partition update.
+ */
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -2793,9 +2808,11 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -2914,10 +2931,12 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -3052,13 +3071,26 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source
+ * and destination partitions, respectively, of a cross-partition update of
+ * the root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey(); is_crosspart_update must be true in
+ * that case.
+ */
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -3073,12 +3105,19 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot;
+ ResultRelInfo *tupsrc;
+
+ Assert((src_partinfo != NULL && dst_partinfo != NULL) ||
+ !is_crosspart_update);
+
+ tupsrc = src_partinfo ? src_partinfo : relinfo;
+ oldslot = ExecGetTriggerOldSlot(estate, tupsrc);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ tupsrc,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,10 +3127,14 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
- true, oldslot, newslot, recheckIndexes,
+ AfterTriggerSaveEvent(estate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
+ true,
+ oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -3214,8 +3257,11 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
- false, NULL, NULL, NIL, NULL, NULL);
+ AfterTriggerSaveEvent(estate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
+ false, NULL, NULL, NIL, NULL, NULL,
+ false);
}
@@ -3496,9 +3542,9 @@ typedef SetConstraintStateData *SetConstraintState;
* Per-trigger-event data
*
* The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS
- * status bits and up to two tuple CTIDs. Each event record also has an
- * associated AfterTriggerSharedData that is shared across all instances of
- * similar events within a "chunk".
+ * status bits, up to two tuple CTIDs, and optionally two OIDs of partitions
+ * Each event record also has an associated AfterTriggerSharedData that is
+ * shared across all instances of similar events within a "chunk".
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
@@ -3509,6 +3555,10 @@ typedef SetConstraintStateData *SetConstraintState;
* tuple(s). This permits storing tuples once regardless of the number of
* row-level triggers on a foreign table.
*
+ * When updates move tuples in partitioned tables to different partitions,
+ * the OIDs of both partitions are stored too, so that the tuples can be
+ * fetched.
+ *
* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
* the right order. To ensure that, we forbid deferrable (constraint)
@@ -3531,16 +3581,16 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
-#define AFTER_TRIGGER_DONE 0x10000000
-#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_DONE 0x80000000
+#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
/* bits describing the size and tuple sources of this event */
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
-#define AFTER_TRIGGER_1CTID 0x40000000
-#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
-
+#define AFTER_TRIGGER_FDW_FETCH 0x20000000
+#define AFTER_TRIGGER_1CTID 0x10000000
+#define AFTER_TRIGGER_2CTID 0x30000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0x38000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
typedef struct AfterTriggerSharedData
@@ -3560,8 +3610,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to fetch
+ * the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags;
+ ItemPointerData ate_ctid1;
+ ItemPointerData ate_ctid2;
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3576,11 +3642,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3830,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4086,8 +4156,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4101,6 +4179,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4108,6 +4188,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4188,12 +4270,35 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into the
+ * target (root partitioned) table slot, converting if needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4203,16 +4308,40 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_CP_UPDATE)) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4441,13 +4570,17 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
+
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4472,12 +4605,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5672,14 +5826,35 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE; in this case, this function is called with relinfo as the
+ * partitioned table, and src_partinfo and dst_partinfo referring to the
+ * source and target leaf partitions, respectively.
+ *
+ * is_crosspart_update is true either when a DELETE event is fired on the
+ * source partition (which is to be ignored) or an UPDATE event is fired on
+ * the root partitioned table.
* ----------
*/
static void
AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
Relation rel = relinfo->ri_RelationDesc;
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5788,6 +5963,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partition update. In that case,
+ * nodeModifyTable.c:ExecCrossPartitionUpdateForeignKey() calls here to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (is_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5848,6 +6036,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part =
+ RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part =
+ RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5872,13 +6073,53 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
+ /* Determine flags */
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
- new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ {
+ if (row_trigger && event == TRIGGER_EVENT_UPDATE)
+ {
+ if (relkind == RELKIND_PARTITIONED_TABLE)
+ new_event.ate_flags = AFTER_TRIGGER_CP_UPDATE;
+ else
+ new_event.ate_flags = AFTER_TRIGGER_2CTID;
+ }
+ else
+ new_event.ate_flags = AFTER_TRIGGER_1CTID;
+ }
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the
+ * loop below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5907,13 +6148,30 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
/*
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
- * tell by inspection that the FK constraint will still pass.
+ * tell by inspection that the FK constraint will still pass. There
+ * are also some cases during cross-partition updates of a partitioned
+ * table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the
+ * update event that will be fired on the root
+ * (partitioned) target table will be used to perform the
+ * necessary foreign key enforcement action.
+ */
+ if (is_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5924,8 +6182,20 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -5934,7 +6204,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a
+ * partitioned table, because the same row trigger must be
+ * present in the leaf partition(s) that are affected as
+ * part of this update and the events fired on them are
+ * queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 549d9eb696..880b135058 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1279,7 +1279,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1331,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1447,8 +1448,22 @@ ExecCloseResultRelations(EState *estate)
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Don't close the root ancestor relation, because that one's
+ * closed in ExecCloseRangeTableRelations().
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 313c87398b..f978e28ba9 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -517,8 +517,9 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
- recheckIndexes, NULL);
+ recheckIndexes, NULL, false);
list_free(recheckIndexes);
}
@@ -557,7 +558,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo,
- tid, NULL, NULL);
+ tid, NULL, NULL, false);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 5ec699a9bd..204126a29f 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -38,6 +38,7 @@
#include "access/tableam.h"
#include "access/xact.h"
#include "catalog/catalog.h"
+#include "catalog/partition.h"
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
@@ -584,6 +585,9 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
* to access "junk" columns that are not going to be stored.
*
* Returns RETURNING result if any, otherwise NULL.
+ * *inserted_tuple is the tuple that's effectively inserted;
+ * *inserted_destrel is the relation where it was inserted.
+ * These are only set on success. FIXME -- see what happens on the "do nothing" cases.
*
* This may change the currently active tuple conversion map in
* mtstate->mt_transition_capture, so the callers must take care to
@@ -596,7 +600,9 @@ ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
Relation resultRelationDesc;
List *recheckIndexes = NIL;
@@ -956,11 +962,14 @@ ExecInsert(ModifyTableState *mtstate,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -994,6 +1003,11 @@ ExecInsert(ModifyTableState *mtstate,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1347,11 +1361,13 @@ ldelete:;
&& mtstate->mt_transition_capture->tcs_update_old_table)
{
ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
tupleid,
oldtuple,
NULL,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1362,7 +1378,7 @@ ldelete:;
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
- ar_delete_trig_tcs);
+ ar_delete_trig_tcs, changingPart);
/* Process RETURNING if present and if requested */
if (processReturning && resultRelInfo->ri_projectReturning)
@@ -1422,7 +1438,7 @@ ldelete:;
* for the caller.
*
* False is returned if the tuple we're trying to move is found to have been
- * concurrently updated. In that case, the caller must to check if the
+ * concurrently updated. In that case, the caller must check if the
* updated tuple that's returned in *retry_slot still needs to be re-routed,
* and call this function again or perform a regular update accordingly.
*/
@@ -1433,7 +1449,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
TupleTableSlot *slot, TupleTableSlot *planSlot,
EPQState *epqstate, bool canSetTag,
TupleTableSlot **retry_slot,
- TupleTableSlot **inserted_tuple)
+ TupleTableSlot **returning_slot,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
EState *estate = mtstate->ps.state;
TupleConversionMap *tupconv_map;
@@ -1556,8 +1574,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
mtstate->mt_root_tuple_slot);
/* Tuple routing starts from the root table. */
- *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
- planSlot, estate, canSetTag);
+ *returning_slot = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot,
+ planSlot, estate, canSetTag, inserted_tuple,
+ insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1570,6 +1589,123 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
return true;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ */
+static List *
+GetAncestorResultRels(ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /* We use ri_RootResultRelInfo for the root ancestor. */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* No need to make ri_RangeTableIndex valid. */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL, 0);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
+/*
+ * Queues up an update event using the target root partitioned table's
+ * trigger to check that a cross-partition update hasn't broken any foreign
+ * keys pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ModifyTableState *mtstate,
+ EState *estate,
+ ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ List *ancestorRels = GetAncestorResultRels(sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell if both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ if (rInfo == rootRelInfo)
+ break;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ int i;
+
+ for (i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(estate,
+ rootRelInfo, sourcePartInfo, destPartInfo,
+ tupleid, NULL, newslot, NIL, NULL, true);
+}
+
/* ----------------------------------------------------------------
* ExecUpdate
*
@@ -1742,9 +1878,12 @@ lreplace:;
*/
if (partition_constraint_failed)
{
- TupleTableSlot *inserted_tuple,
+ TupleTableSlot *oldslot = slot,
+ *inserted_tuple,
+ *returning_slot = NULL,
*retry_slot;
bool retry;
+ ResultRelInfo *insert_destrel = NULL;
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
@@ -1756,14 +1895,39 @@ lreplace:;
retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
oldtuple, slot, planSlot,
epqstate, canSetTag,
- &retry_slot, &inserted_tuple);
+ &retry_slot, &returning_slot,
+ &inserted_tuple,
+ &insert_destrel);
if (retry)
{
slot = retry_slot;
goto lreplace;
}
- return inserted_tuple;
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in
+ * non-cross-partition update situations, because the leaf
+ * partition's AR update triggers will take care of that. During
+ * cross-partition updates implemented as delete on the source
+ * partition followed by insert on the destination partition,
+ * AR-UPDATE triggers of the root table (that is, the table
+ * mentioned in the query) must be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(mtstate, estate,
+ resultRelInfo,
+ insert_destrel,
+ tupleid, oldslot,
+ inserted_tuple);
+
+ return returning_slot;
}
/*
@@ -1942,11 +2106,15 @@ lreplace:;
(estate->es_processed)++;
/* AFTER ROW UPDATE Triggers */
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot,
+ ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
list_free(recheckIndexes);
@@ -2559,7 +2727,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(node, resultRelInfo, slot, planSlot,
- estate, node->canSetTag);
+ estate, node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
/* Initialize projection info if first time for this table */
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index c95cd32402..01d4c22cfc 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index e1271420e5..66bf6c16e3 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -214,7 +214,8 @@ extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -231,11 +232,14 @@ extern bool ExecBRUpdateTriggers(EState *estate,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 344399f6a8..03c587730c 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,8 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 4ea8735dd8..37ad2b7663 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -529,6 +529,12 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /*
+ * Used when a leaf partition is involved in a cross-partition update of
+ * one of its ancestors; see ExecCrossPartitionUpdateForeignKey().
+ */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.24.1
On 2022-Jan-19, Amit Langote wrote:
BTW, your tweaks patch added this:
+ * *inserted_tuple is the tuple that's effectively inserted; + * *inserted_destrel is the relation where it was inserted. + * These are only set on success. FIXME -- see what happens on the "do nothing" cases.If by "do nothing cases" you mean INSERT ON CONFLICT ... DO NOTHING,
then I don't think it matters, because the caller in that case would
be ExecModifyTable() which doesn't care about inserted_tuple and
inserted_destrel.
No, I meant a FOR EACH ROW trigger that does RETURN NULL to "abort" the
insertion. IIRC in non-partitioned cases it is possibly to break
referential integrity by using those. What I was wondering is whether
you can make this new code crash.
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
"I can see support will not be a problem. 10 out of 10." (Simon Wittber)
(http://archives.postgresql.org/pgsql-general/2004-12/msg00159.php)
On Wed, Jan 19, 2022 at 6:26 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Jan-19, Amit Langote wrote:
BTW, your tweaks patch added this:
+ * *inserted_tuple is the tuple that's effectively inserted; + * *inserted_destrel is the relation where it was inserted. + * These are only set on success. FIXME -- see what happens on the "do nothing" cases.If by "do nothing cases" you mean INSERT ON CONFLICT ... DO NOTHING,
then I don't think it matters, because the caller in that case would
be ExecModifyTable() which doesn't care about inserted_tuple and
inserted_destrel.No, I meant a FOR EACH ROW trigger that does RETURN NULL to "abort" the
insertion.
Ah, gotcha.
IIRC in non-partitioned cases it is possibly to break
referential integrity by using those. What I was wondering is whether
you can make this new code crash.
insert_destrel would be left set to NULL, which means
ExecCrossPartitionUpdateForeignKey() won't get called, because:
* NULL insert_destrel means that the move failed to occur, that
* is, the update failed, so no need to anything in that case.
*/
if (insert_destrel &&
resultRelInfo->ri_TrigDesc &&
resultRelInfo->ri_TrigDesc->trig_update_after_row)
ExecCrossPartitionUpdateForeignKey(mtstate, estate,
Moreover, trigger documentation warns of a "possibility of surprising
outcomes" if BR triggers are present in partitions that are chosen as
destinations of cross-partition updates:
"Then all row-level BEFORE INSERT triggers are fired on the
destination partition. The possibility of surprising outcomes should
be considered when all these triggers affect the row being moved."
I suppose the new code shouldn't need to take special care in such cases either.
--
Amit Langote
EDB: http://www.enterprisedb.com
I rebased this patch; v15 attached. Other than fixing the (very large)
conflicts due to nodeModifyTable.c rework, the most important change is
moving GetAncestorResultRels into execMain.c and renaming it to have the
"Exec-" prefix. The reason is that what this code is doing is affect
struct ResultRelInfo, which is owned by execMain.c, so it seemed bogus
to do that in nodeModifyTable.c and then let execMain.c's
ExecCloseResultRelations() do the cleanup. I added a little commentary
in the latter routine too.
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
"World domination is proceeding according to plan" (Andrew Morton)
Attachments:
v15-0001-Enforce-foreign-key-correctly-during-cross-parti.patchtext/x-diff; charset=utf-8Download
From 345ed49718708d8ebde9e2dcf06bf963190bc5c8 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Fri, 18 Mar 2022 11:01:24 +0100
Subject: [PATCH v15] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
Author: Amit Langote
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 389 +++++++++++++++++++---
src/backend/executor/execMain.c | 86 ++++-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 155 ++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 8 +-
src/include/executor/executor.h | 4 +-
src/include/nodes/execnodes.h | 6 +
src/test/regress/expected/foreign_key.out | 204 +++++++++++-
src/test/regress/sql/foreign_key.sql | 135 +++++++-
11 files changed, 925 insertions(+), 80 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index e08bd9a370..a9aa043981 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -95,10 +95,13 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
Instrumentation *instr,
MemoryContext per_tuple_context);
static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
@@ -2458,8 +2461,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_INSERT,
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
bool
@@ -2547,10 +2552,12 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -2672,8 +2679,10 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_DELETE,
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
/*
@@ -2768,11 +2777,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
return result;
}
+/*
+ * Note: is_crosspart_update must be true if the DELETE is being performed
+ * as part of a cross-partition update.
+ */
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -2793,9 +2808,11 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -2914,10 +2931,12 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -3052,13 +3071,26 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source
+ * and destination partitions, respectively, of a cross-partition update of
+ * the root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey(); is_crosspart_update must be true in
+ * that case.
+ */
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -3073,12 +3105,19 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot;
+ ResultRelInfo *tupsrc;
+
+ Assert((src_partinfo != NULL && dst_partinfo != NULL) ||
+ !is_crosspart_update);
+
+ tupsrc = src_partinfo ? src_partinfo : relinfo;
+ oldslot = ExecGetTriggerOldSlot(estate, tupsrc);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ tupsrc,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,10 +3127,14 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
- true, oldslot, newslot, recheckIndexes,
+ AfterTriggerSaveEvent(estate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
+ true,
+ oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -3214,8 +3257,11 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
- false, NULL, NULL, NIL, NULL, NULL);
+ AfterTriggerSaveEvent(estate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
+ false, NULL, NULL, NIL, NULL, NULL,
+ false);
}
@@ -3496,9 +3542,9 @@ typedef SetConstraintStateData *SetConstraintState;
* Per-trigger-event data
*
* The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS
- * status bits and up to two tuple CTIDs. Each event record also has an
- * associated AfterTriggerSharedData that is shared across all instances of
- * similar events within a "chunk".
+ * status bits, up to two tuple CTIDs, and optionally two OIDs of partitions
+ * Each event record also has an associated AfterTriggerSharedData that is
+ * shared across all instances of similar events within a "chunk".
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
@@ -3509,6 +3555,10 @@ typedef SetConstraintStateData *SetConstraintState;
* tuple(s). This permits storing tuples once regardless of the number of
* row-level triggers on a foreign table.
*
+ * When updates move tuples in partitioned tables to different partitions,
+ * the OIDs of both partitions are stored too, so that the tuples can be
+ * fetched.
+ *
* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
* the right order. To ensure that, we forbid deferrable (constraint)
@@ -3531,16 +3581,16 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
-#define AFTER_TRIGGER_DONE 0x10000000
-#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_DONE 0x80000000
+#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
/* bits describing the size and tuple sources of this event */
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
-#define AFTER_TRIGGER_1CTID 0x40000000
-#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
-
+#define AFTER_TRIGGER_FDW_FETCH 0x20000000
+#define AFTER_TRIGGER_1CTID 0x10000000
+#define AFTER_TRIGGER_2CTID 0x30000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0x38000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
typedef struct AfterTriggerSharedData
@@ -3560,8 +3610,24 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to fetch
+ * the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags;
+ ItemPointerData ate_ctid1;
+ ItemPointerData ate_ctid2;
+} AfterTriggerEventDataNoOids;
+
/* AfterTriggerEventData, minus ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
@@ -3576,11 +3642,13 @@ typedef struct AfterTriggerEventDataZeroCtids
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3830,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4096,8 +4166,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4111,6 +4189,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4118,6 +4198,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4198,12 +4280,35 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into the
+ * target (root partitioned) table slot, converting if needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4213,16 +4318,40 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_CP_UPDATE)) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4451,13 +4580,17 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
+
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4482,12 +4615,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5767,14 +5921,35 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE; in this case, this function is called with relinfo as the
+ * partitioned table, and src_partinfo and dst_partinfo referring to the
+ * source and target leaf partitions, respectively.
+ *
+ * is_crosspart_update is true either when a DELETE event is fired on the
+ * source partition (which is to be ignored) or an UPDATE event is fired on
+ * the root partitioned table.
* ----------
*/
static void
AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
Relation rel = relinfo->ri_RelationDesc;
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5854,6 +6029,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partition update. In that case,
+ * nodeModifyTable.c:ExecCrossPartitionUpdateForeignKey() calls here to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (is_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5914,6 +6102,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part =
+ RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part =
+ RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5938,13 +6139,53 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
+ /* Determine flags */
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
- new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ {
+ if (row_trigger && event == TRIGGER_EVENT_UPDATE)
+ {
+ if (relkind == RELKIND_PARTITIONED_TABLE)
+ new_event.ate_flags = AFTER_TRIGGER_CP_UPDATE;
+ else
+ new_event.ate_flags = AFTER_TRIGGER_2CTID;
+ }
+ else
+ new_event.ate_flags = AFTER_TRIGGER_1CTID;
+ }
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the
+ * loop below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5973,13 +6214,30 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
/*
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
- * tell by inspection that the FK constraint will still pass.
+ * tell by inspection that the FK constraint will still pass. There
+ * are also some cases during cross-partition updates of a partitioned
+ * table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the
+ * update event that will be fired on the root
+ * (partitioned) target table will be used to perform the
+ * necessary foreign key enforcement action.
+ */
+ if (is_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5990,8 +6248,20 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -6000,7 +6270,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a
+ * partitioned table, because the same row trigger must be
+ * present in the leaf partition(s) that are affected as
+ * part of this update and the events fired on them are
+ * queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 549d9eb696..473d2e00a2 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -44,6 +44,7 @@
#include "access/transam.h"
#include "access/xact.h"
#include "catalog/namespace.h"
+#include "catalog/partition.h"
#include "catalog/pg_publication.h"
#include "commands/matview.h"
#include "commands/trigger.h"
@@ -1279,7 +1280,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1332,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1344,6 +1346,69 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
return rInfo;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ *
+ * These work much like the ones opened by ExecGetTriggerResultRel, except
+ * that we need to keep them in a separate list.
+ *
+ * These are closed by ExecCloseResultRelations.
+ */
+List *
+ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /*
+ * Ignore the root ancestor here, and use ri_RootResultRelInfo
+ * (below) for it instead. Also, we stop climbing up the
+ * hierarchy when we find the table that was mentioned in the
+ * query.
+ */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* dummy rangetable index */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL,
+ estate->es_instrument);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ /* We must have found some ancestor */
+ Assert(resultRelInfo->ri_ancestorResultRels != NIL);
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
/* ----------------------------------------------------------------
* ExecPostprocessPlan
*
@@ -1443,12 +1508,29 @@ ExecCloseResultRelations(EState *estate)
/*
* close indexes of result relation(s) if any. (Rels themselves are
* closed in ExecCloseRangeTableRelations())
+ *
+ * In addition, close the stub RTs that may be in each resultrel's
+ * ri_ancestorResultRels.
*/
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Ancestors with RTI > 0 (should only be the root ancestor) are
+ * closed by ExecCloseRangeTableRelations.
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 09f78f2244..13328141e2 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -517,8 +517,9 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
- recheckIndexes, NULL);
+ recheckIndexes, NULL, false);
list_free(recheckIndexes);
}
@@ -557,7 +558,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo,
- tid, NULL, NULL);
+ tid, NULL, NULL, false);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 6239abae90..46f7ba6d84 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -122,6 +122,12 @@ static void ExecBatchInsert(ModifyTableState *mtstate,
int numSlots,
EState *estate,
bool canSetTag);
+static void ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
+ ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot);
static bool ExecOnConflictUpdate(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer conflictTid,
@@ -635,6 +641,9 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
* slot contains the new tuple value to be stored.
*
* Returns RETURNING result if any, otherwise NULL.
+ * *inserted_tuple is the tuple that's effectively inserted;
+ * *inserted_destrel is the relation where it was inserted.
+ * These are only set on success.
*
* This may change the currently active tuple conversion map in
* mtstate->mt_transition_capture, so the callers must take care to
@@ -645,7 +654,9 @@ static TupleTableSlot *
ExecInsert(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
TupleTableSlot *slot,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
ModifyTableState *mtstate = context->mtstate;
EState *estate = context->estate;
@@ -1008,11 +1019,14 @@ ExecInsert(ModifyTableContext *context,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1046,6 +1060,11 @@ ExecInsert(ModifyTableContext *context,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1160,7 +1179,7 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
*/
static void
ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
- ItemPointer tupleid, HeapTuple oldtuple)
+ ItemPointer tupleid, HeapTuple oldtuple, bool changingPart)
{
ModifyTableState *mtstate = context->mtstate;
EState *estate = context->estate;
@@ -1176,8 +1195,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture &&
mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple,
- NULL, NULL, mtstate->mt_transition_capture);
+ ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ NULL, NULL, mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1188,7 +1210,7 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
- ar_delete_trig_tcs);
+ ar_delete_trig_tcs, changingPart);
}
/* ----------------------------------------------------------------
@@ -1457,7 +1479,7 @@ ldelete:;
if (tupleDeleted)
*tupleDeleted = true;
- ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple);
+ ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple, changingPart);
/* Process RETURNING if present and if requested */
if (processReturning && resultRelInfo->ri_projectReturning)
@@ -1517,8 +1539,8 @@ ldelete:;
* for the caller.
*
* False is returned if the tuple we're trying to move is found to have been
- * concurrently updated. In that case, the caller must to check if the
- * updated tuple that's returned in *retry_slot still needs to be re-routed,
+ * concurrently updated. In that case, the caller must check if the
+ * updated tuple (in updateCxt->cpUpdateRetrySlot) still needs to be re-routed,
* and call this function again or perform a regular update accordingly.
*/
static bool
@@ -1526,7 +1548,10 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer tupleid, HeapTuple oldtuple,
TupleTableSlot *slot,
- bool canSetTag, UpdateContext *updateCxt)
+ bool canSetTag,
+ UpdateContext *updateCxt,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
ModifyTableState *mtstate = context->mtstate;
EState *estate = mtstate->ps.state;
@@ -1652,7 +1677,8 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
/* Tuple routing starts from the root table. */
context->cpUpdateReturningSlot =
- ExecInsert(context, mtstate->rootResultRelInfo, slot, canSetTag);
+ ExecInsert(context, mtstate->rootResultRelInfo, slot, canSetTag,
+ inserted_tuple, insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1793,6 +1819,9 @@ lreplace:;
*/
if (partition_constraint_failed)
{
+ TupleTableSlot *inserted_tuple;
+ ResultRelInfo *insert_destrel;
+
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
* partition it's currently in and then insert it back into the root
@@ -1801,11 +1830,37 @@ lreplace:;
*/
if (ExecCrossPartitionUpdate(context, resultRelInfo,
tupleid, oldtuple, slot,
- canSetTag, updateCxt))
+ canSetTag, updateCxt,
+ &inserted_tuple,
+ &insert_destrel))
{
/* success! */
updateCxt->updated = true;
updateCxt->crossPartUpdate = true;
+
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in
+ * non-cross-partition update situations, because the leaf
+ * partition's AR update triggers will take care of that. During
+ * cross-partition updates implemented as delete on the source
+ * partition followed by insert on the destination partition,
+ * AR-UPDATE triggers of the root table (that is, the table
+ * mentioned in the query) must be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(context,
+ resultRelInfo,
+ insert_destrel,
+ tupleid, slot,
+ inserted_tuple);
+
return TM_Ok;
}
@@ -1871,11 +1926,13 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(context->estate, resultRelInfo,
+ NULL, NULL,
tupleid, oldtuple, slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* Check any WITH CHECK OPTION constraints from parent views. We are
@@ -1891,6 +1948,74 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
slot, context->estate);
}
+/*
+ * Queues up an update event using the target root partitioned table's
+ * trigger to check that a cross-partition update hasn't broken any foreign
+ * keys pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
+ ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo;
+ List *ancestorRels;
+
+ rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ ancestorRels = ExecGetAncestorResultRels(context->estate, sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell that both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ /* Ignore the root ancestor, because ...?? */
+ if (rInfo == rootRelInfo)
+ continue;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ for (int i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(context->estate,
+ rootRelInfo, sourcePartInfo, destPartInfo,
+ tupleid, NULL, newslot, NIL, NULL, true);
+}
/* ----------------------------------------------------------------
* ExecUpdate
@@ -2745,7 +2870,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(&context, resultRelInfo, slot,
- node->canSetTag);
+ node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index c95cd32402..01d4c22cfc 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index e1271420e5..66bf6c16e3 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -214,7 +214,8 @@ extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -231,11 +232,14 @@ extern bool ExecBRUpdateTriggers(EState *estate,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 344399f6a8..82925b4b63 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,9 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
+extern List *ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index dd95dc40c7..44dd73fc80 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -530,6 +530,12 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /*
+ * Used when a leaf partition is involved in a cross-partition update of
+ * one of its ancestors; see ExecCrossPartitionUpdateForeignKey().
+ */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.30.2
On Fri, Mar 18, 2022 at 9:38 AM Alvaro Herrera <alvherre@alvh.no-ip.org>
wrote:
I rebased this patch; v15 attached. Other than fixing the (very large)
conflicts due to nodeModifyTable.c rework, the most important change is
moving GetAncestorResultRels into execMain.c and renaming it to have the
"Exec-" prefix. The reason is that what this code is doing is affect
struct ResultRelInfo, which is owned by execMain.c, so it seemed bogus
to do that in nodeModifyTable.c and then let execMain.c's
ExecCloseResultRelations() do the cleanup. I added a little commentary
in the latter routine too.--
Álvaro Herrera Valdivia, Chile —
https://www.EnterpriseDB.com/
"World domination is proceeding according to plan" (Andrew Morton)
Hi,
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order
bits */
+#define AFTER_TRIGGER_DONE 0x80000000
+#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
Is it better if the order of AFTER_TRIGGER_DONE
and AFTER_TRIGGER_IN_PROGRESS is swapped (for the ordinal values to be
sequential) ?
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
It would be better to add a comment for this constant, explaining what CP
means (cross partition).
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result
relation");
It would be better to include the relation name in the error message.
+ /* Ignore the root ancestor, because ...?? */
Please fill out the remainder of the comment.
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
The variable says fkey, but the constant is not RI_TRIGGER_FK. Maybe add a
comment explaining why.
Cheers
On 2022-Mar-18, Zhihong Yu wrote:
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */ +#define AFTER_TRIGGER_DONE 0x80000000 +#define AFTER_TRIGGER_IN_PROGRESS 0x40000000Is it better if the order of AFTER_TRIGGER_DONE
and AFTER_TRIGGER_IN_PROGRESS is swapped (for the ordinal values to be
sequential) ?
They *are* sequential -- See
/messages/by-id/202201172215.2tse3vjjgi2b@alvherre.pgsql
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
It would be better to add a comment for this constant, explaining what CP
means (cross partition).
Sure.
+ if (!partRel->rd_rel->relispartition) + elog(ERROR, "cannot find ancestors of a non-partition result relation");It would be better to include the relation name in the error message.
I don't think it matters. We don't really expect to hit this.
+ /* Ignore the root ancestor, because ...?? */
Please fill out the remainder of the comment.
I actually would like to know what's the rationale for this myself.
Amit?
+ if (!trig->tgisclone && + RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK) + { + has_noncloned_fkey = true;The variable says fkey, but the constant is not RI_TRIGGER_FK. Maybe add a
comment explaining why.
Well, the constant is about the trigger *function*, not about any
constraint. This code is testing "is this a noncloned trigger, and does
that trigger use an FK-related function?" If you have a favorite
comment to include, I'm all ears.
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
"It takes less than 2 seconds to get to 78% complete; that's a good sign.
A few seconds later it's at 90%, but it seems to have stuck there. Did
somebody make percentages logarithmic while I wasn't looking?"
http://smylers.hates-software.com/2005/09/08/1995c749.html
Attachments:
v16-0001-Enforce-foreign-key-correctly-during-cross-parti.patchtext/x-diff; charset=utf-8Download
From a71baa9ab81d6f9ed04ce2c37c86d806ef36aa8b Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Fri, 18 Mar 2022 11:01:24 +0100
Subject: [PATCH v16] Enforce foreign key correctly during cross-partition
updates
When an update on a partitioned table referenced in foreign keys
constraint causes a row to move from one partition to another, which
is implemented by deleting the old row from the source leaf partition
followed by inserting the new row into the destination leaf partition,
firing the foreign key triggers on that delete event can result in
surprising outcomes for those keys. For example, a given foreign
key's delete trigger which implements the ON DELETE CASCADE clause of
that key will delete any referencing rows when triggerred for that
internal DELETE, although it should not, because the referenced row
is simply being moved from one partition of the referenced root
partitioned table into another, not being deleted from it.
This commit teaches trigger.c to skip queuing such delete trigger
events on the leaf partitions in favor of an UPDATE event fired on
the root target relation. Doing so makes sense because both the
old and the new tuple "logically" belong to the root relation.
The after trigger event queuing interface now allows passing the
source and the destination partitions of a particular cross-partition
update when registering the update event for the root partitioned
table. Along with the 2 ctids of the old and the new tuple, an after
trigger event now also stores the OIDs of those partitions. The tuples
fetched from the source and the destination partitions are converted
into the root table format before they are passed to the trigger
function.
The implementation currently has a limitation that only the foreign
keys pointing into the query's target relation are considered, not
those of its sub-partitioned partitions. That seems like a
reasonable limitation, because it sounds rare to have distinct
foreign keys pointing into sub-partitioned partitions, but not into
the root table.
Author: Amit Langote
---
doc/src/sgml/ref/update.sgml | 7 +
src/backend/commands/trigger.c | 394 +++++++++++++++++++---
src/backend/executor/execMain.c | 86 ++++-
src/backend/executor/execReplication.c | 5 +-
src/backend/executor/nodeModifyTable.c | 151 ++++++++-
src/backend/utils/adt/ri_triggers.c | 6 +
src/include/commands/trigger.h | 8 +-
src/include/executor/executor.h | 4 +-
src/include/nodes/execnodes.h | 6 +
src/test/regress/expected/foreign_key.out | 204 ++++++++++-
src/test/regress/sql/foreign_key.sql | 135 +++++++-
11 files changed, 926 insertions(+), 80 deletions(-)
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3fa54e5f70..3ba13010e7 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -316,6 +316,13 @@ UPDATE <replaceable class="parameter">count</replaceable>
partition (provided the foreign data wrapper supports tuple routing), they
cannot be moved from a foreign-table partition to another partition.
</para>
+
+ <para>
+ An attempt of moving a row from one partition to another will fail if a
+ foreign key is found to directly reference a non-root partitioned table
+ in the partition tree, unless that table is also directly mentioned
+ in the <command>UPDATE</command>query.
+ </para>
</refsect1>
<refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index e08bd9a370..0adecdea22 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -95,10 +95,13 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
Instrumentation *instr,
MemoryContext per_tuple_context);
static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldtup, TupleTableSlot *newtup,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
static void AfterTriggerEnlargeQueryState(void);
static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
@@ -2458,8 +2461,10 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_insert_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_INSERT,
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
bool
@@ -2547,10 +2552,12 @@ ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
if ((trigdesc && trigdesc->trig_insert_after_row) ||
(transition_capture && transition_capture->tcs_insert_new_table))
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_INSERT,
true, NULL, slot,
recheckIndexes, NULL,
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -2672,8 +2679,10 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_delete_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
- false, NULL, NULL, NIL, NULL, transition_capture);
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_DELETE,
+ false, NULL, NULL, NIL, NULL, transition_capture,
+ false);
}
/*
@@ -2768,11 +2777,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
return result;
}
+/*
+ * Note: is_crosspart_update must be true if the DELETE is being performed
+ * as part of a cross-partition update.
+ */
void
-ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
+ExecARDeleteTriggers(EState *estate,
+ ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -2793,9 +2808,11 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_DELETE,
true, slot, NULL, NIL, NULL,
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -2914,10 +2931,12 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
Assert(relinfo->ri_RootResultRelInfo == NULL);
if (trigdesc && trigdesc->trig_update_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
+ AfterTriggerSaveEvent(estate, relinfo, NULL, NULL,
+ TRIGGER_EVENT_UPDATE,
false, NULL, NULL, NIL,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ false);
}
bool
@@ -3052,13 +3071,26 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
+/*
+ * Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source
+ * and destination partitions, respectively, of a cross-partition update of
+ * the root partitioned table mentioned in the query, given by 'relinfo'.
+ * 'tupleid' in that case refers to the ctid of the "old" tuple in the source
+ * partition, and 'newslot' contains the "new" tuple in the destination
+ * partition. This interface allows to support the requirements of
+ * ExecCrossPartitionUpdateForeignKey(); is_crosspart_update must be true in
+ * that case.
+ */
void
ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -3073,12 +3105,19 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
* separately for DELETE and INSERT to capture transition table rows.
* In such case, either old tuple or new tuple can be NULL.
*/
- TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
+ TupleTableSlot *oldslot;
+ ResultRelInfo *tupsrc;
+
+ Assert((src_partinfo != NULL && dst_partinfo != NULL) ||
+ !is_crosspart_update);
+
+ tupsrc = src_partinfo ? src_partinfo : relinfo;
+ oldslot = ExecGetTriggerOldSlot(estate, tupsrc);
if (fdw_trigtuple == NULL && ItemPointerIsValid(tupleid))
GetTupleForTrigger(estate,
NULL,
- relinfo,
+ tupsrc,
tupleid,
LockTupleExclusive,
oldslot,
@@ -3088,10 +3127,14 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
else
ExecClearTuple(oldslot);
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
- true, oldslot, newslot, recheckIndexes,
+ AfterTriggerSaveEvent(estate, relinfo,
+ src_partinfo, dst_partinfo,
+ TRIGGER_EVENT_UPDATE,
+ true,
+ oldslot, newslot, recheckIndexes,
ExecGetAllUpdatedCols(relinfo, estate),
- transition_capture);
+ transition_capture,
+ is_crosspart_update);
}
}
@@ -3214,8 +3257,11 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
if (trigdesc && trigdesc->trig_truncate_after_statement)
- AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
- false, NULL, NULL, NIL, NULL, NULL);
+ AfterTriggerSaveEvent(estate, relinfo,
+ NULL, NULL,
+ TRIGGER_EVENT_TRUNCATE,
+ false, NULL, NULL, NIL, NULL, NULL,
+ false);
}
@@ -3496,9 +3542,9 @@ typedef SetConstraintStateData *SetConstraintState;
* Per-trigger-event data
*
* The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS
- * status bits and up to two tuple CTIDs. Each event record also has an
- * associated AfterTriggerSharedData that is shared across all instances of
- * similar events within a "chunk".
+ * status bits, up to two tuple CTIDs, and optionally two OIDs of partitions
+ * Each event record also has an associated AfterTriggerSharedData that is
+ * shared across all instances of similar events within a "chunk".
*
* For row-level triggers, we arrange not to waste storage on unneeded ctid
* fields. Updates of regular tables use two; inserts and deletes of regular
@@ -3509,6 +3555,11 @@ typedef SetConstraintStateData *SetConstraintState;
* tuple(s). This permits storing tuples once regardless of the number of
* row-level triggers on a foreign table.
*
+ * When updates move tuples in partitioned tables to different partitions,
+ * the OIDs of both partitions are stored too, so that the tuples can be
+ * fetched; such entries are marked AFTER_TRIGGER_CP_UPDATE (for "cross-
+ * partition").
+ *
* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
* the right order. To ensure that, we forbid deferrable (constraint)
@@ -3531,16 +3582,16 @@ typedef SetConstraintStateData *SetConstraintState;
*/
typedef uint32 TriggerFlags;
-#define AFTER_TRIGGER_OFFSET 0x0FFFFFFF /* must be low-order bits */
-#define AFTER_TRIGGER_DONE 0x10000000
-#define AFTER_TRIGGER_IN_PROGRESS 0x20000000
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */
+#define AFTER_TRIGGER_DONE 0x80000000
+#define AFTER_TRIGGER_IN_PROGRESS 0x40000000
/* bits describing the size and tuple sources of this event */
#define AFTER_TRIGGER_FDW_REUSE 0x00000000
-#define AFTER_TRIGGER_FDW_FETCH 0x80000000
-#define AFTER_TRIGGER_1CTID 0x40000000
-#define AFTER_TRIGGER_2CTID 0xC0000000
-#define AFTER_TRIGGER_TUP_BITS 0xC0000000
-
+#define AFTER_TRIGGER_FDW_FETCH 0x20000000
+#define AFTER_TRIGGER_1CTID 0x10000000
+#define AFTER_TRIGGER_2CTID 0x30000000
+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
+#define AFTER_TRIGGER_TUP_BITS 0x38000000
typedef struct AfterTriggerSharedData *AfterTriggerShared;
typedef struct AfterTriggerSharedData
@@ -3560,27 +3611,45 @@ typedef struct AfterTriggerEventData
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
ItemPointerData ate_ctid2; /* new updated tuple */
+
+ /*
+ * During a cross-partition update of a partitioned table, we also store
+ * the OIDs of source and destination partitions that are needed to fetch
+ * the old (ctid1) and the new tuple (ctid2) from, respectively.
+ */
+ Oid ate_src_part;
+ Oid ate_dst_part;
} AfterTriggerEventData;
-/* AfterTriggerEventData, minus ate_ctid2 */
+/* AfterTriggerEventData, minus ate_src_part, ate_dst_part */
+typedef struct AfterTriggerEventDataNoOids
+{
+ TriggerFlags ate_flags;
+ ItemPointerData ate_ctid1;
+ ItemPointerData ate_ctid2;
+} AfterTriggerEventDataNoOids;
+
+/* AfterTriggerEventData, minus ate_*_part and ate_ctid2 */
typedef struct AfterTriggerEventDataOneCtid
{
TriggerFlags ate_flags; /* status bits and offset to shared data */
ItemPointerData ate_ctid1; /* inserted, deleted, or old updated tuple */
} AfterTriggerEventDataOneCtid;
-/* AfterTriggerEventData, minus ate_ctid1 and ate_ctid2 */
+/* AfterTriggerEventData, minus ate_*_part, ate_ctid1 and ate_ctid2 */
typedef struct AfterTriggerEventDataZeroCtids
{
TriggerFlags ate_flags; /* status bits and offset to shared data */
} AfterTriggerEventDataZeroCtids;
#define SizeofTriggerEvent(evt) \
- (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_CP_UPDATE ? \
sizeof(AfterTriggerEventData) : \
- ((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
- sizeof(AfterTriggerEventDataOneCtid) : \
- sizeof(AfterTriggerEventDataZeroCtids))
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ? \
+ sizeof(AfterTriggerEventDataNoOids) : \
+ (((evt)->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_1CTID ? \
+ sizeof(AfterTriggerEventDataOneCtid) : \
+ sizeof(AfterTriggerEventDataZeroCtids))))
#define GetTriggerSharedData(evt) \
((AfterTriggerShared) ((char *) (evt) + ((evt)->ate_flags & AFTER_TRIGGER_OFFSET)))
@@ -3762,6 +3831,8 @@ static AfterTriggersData afterTriggers;
static void AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo,
Instrumentation *instr,
@@ -4096,8 +4167,16 @@ afterTriggerDeleteHeadEventChunk(AfterTriggersQueryData *qs)
* fmgr lookup cache space at the caller level. (For triggers fired at
* the end of a query, we can even piggyback on the executor's state.)
*
+ * When fired for a cross-partition update of a partitioned table, the old
+ * tuple is fetched using 'src_relInfo' (the source leaf partition) and
+ * the new tuple using 'dst_relInfo' (the destination leaf partition), though
+ * both are converted into the root partitioned table's format before passing
+ * to the trigger function.
+ *
* event: event currently being fired.
- * rel: open relation for event.
+ * relInfo: result relation for event.
+ * src_relInfo: source partition of a cross-partition update
+ * dst_relInfo: its destination partition
* trigdesc: working copy of rel's trigger info.
* finfo: array of fmgr lookup cache entries (one per trigger in trigdesc).
* instr: array of EXPLAIN ANALYZE instrumentation nodes (one per trigger),
@@ -4111,6 +4190,8 @@ static void
AfterTriggerExecute(EState *estate,
AfterTriggerEvent event,
ResultRelInfo *relInfo,
+ ResultRelInfo *src_relInfo,
+ ResultRelInfo *dst_relInfo,
TriggerDesc *trigdesc,
FmgrInfo *finfo, Instrumentation *instr,
MemoryContext per_tuple_context,
@@ -4118,6 +4199,8 @@ AfterTriggerExecute(EState *estate,
TupleTableSlot *trig_tuple_slot2)
{
Relation rel = relInfo->ri_RelationDesc;
+ Relation src_rel = src_relInfo->ri_RelationDesc;
+ Relation dst_rel = dst_relInfo->ri_RelationDesc;
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
@@ -4198,12 +4281,35 @@ AfterTriggerExecute(EState *estate,
default:
if (ItemPointerIsValid(&(event->ate_ctid1)))
{
- LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ TupleTableSlot *src_slot = ExecGetTriggerOldSlot(estate,
+ src_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid1),
+ if (!table_tuple_fetch_row_version(src_rel,
+ &(event->ate_ctid1),
SnapshotAny,
- LocTriggerData.tg_trigslot))
+ src_slot))
elog(ERROR, "failed to fetch tuple1 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the source partition into the
+ * target (root partitioned) table slot, converting if needed.
+ */
+ if (src_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(src_relInfo);
+
+ LocTriggerData.tg_trigslot = ExecGetTriggerOldSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ src_slot,
+ LocTriggerData.tg_trigslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_trigslot, src_slot);
+ }
+ else
+ LocTriggerData.tg_trigslot = src_slot;
LocTriggerData.tg_trigtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_trigslot, false, &should_free_trig);
}
@@ -4213,16 +4319,40 @@ AfterTriggerExecute(EState *estate,
}
/* don't touch ctid2 if not there */
- if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
- AFTER_TRIGGER_2CTID &&
+ if (((event->ate_flags & AFTER_TRIGGER_TUP_BITS) == AFTER_TRIGGER_2CTID ||
+ (event->ate_flags & AFTER_TRIGGER_CP_UPDATE)) &&
ItemPointerIsValid(&(event->ate_ctid2)))
{
- LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ TupleTableSlot *dst_slot = ExecGetTriggerNewSlot(estate,
+ dst_relInfo);
- if (!table_tuple_fetch_row_version(rel, &(event->ate_ctid2),
+ if (!table_tuple_fetch_row_version(dst_rel,
+ &(event->ate_ctid2),
SnapshotAny,
- LocTriggerData.tg_newslot))
+ dst_slot))
elog(ERROR, "failed to fetch tuple2 for AFTER trigger");
+
+ /*
+ * Store the tuple fetched from the destination partition into
+ * the target (root partitioned) table slot, converting if
+ * needed.
+ */
+ if (dst_relInfo != relInfo)
+ {
+ TupleConversionMap *map = ExecGetChildToRootMap(dst_relInfo);
+
+ LocTriggerData.tg_newslot = ExecGetTriggerNewSlot(estate, relInfo);
+ if (map)
+ {
+ execute_attr_map_slot(map->attrMap,
+ dst_slot,
+ LocTriggerData.tg_newslot);
+ }
+ else
+ ExecCopySlot(LocTriggerData.tg_newslot, dst_slot);
+ }
+ else
+ LocTriggerData.tg_newslot = dst_slot;
LocTriggerData.tg_newtuple =
ExecFetchSlotHeapTuple(LocTriggerData.tg_newslot, false, &should_free_new);
}
@@ -4451,13 +4581,17 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
if ((event->ate_flags & AFTER_TRIGGER_IN_PROGRESS) &&
evtshared->ats_firing_id == firing_id)
{
+ ResultRelInfo *src_rInfo,
+ *dst_rInfo;
+
/*
* So let's fire it... but first, find the correct relation if
* this is not the same relation as before.
*/
if (rel == NULL || RelationGetRelid(rel) != evtshared->ats_relid)
{
- rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid);
+ rInfo = ExecGetTriggerResultRel(estate, evtshared->ats_relid,
+ NULL);
rel = rInfo->ri_RelationDesc;
/* Catch calls with insufficient relcache refcounting */
Assert(!RelationHasReferenceCountZero(rel));
@@ -4482,12 +4616,33 @@ afterTriggerInvokeEvents(AfterTriggerEventList *events,
evtshared->ats_relid);
}
+ /*
+ * Look up source and destination partition result rels of a
+ * cross-partition update event.
+ */
+ if ((event->ate_flags & AFTER_TRIGGER_TUP_BITS) ==
+ AFTER_TRIGGER_CP_UPDATE)
+ {
+ Assert(OidIsValid(event->ate_src_part) &&
+ OidIsValid(event->ate_dst_part));
+ src_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_src_part,
+ rInfo);
+ dst_rInfo = ExecGetTriggerResultRel(estate,
+ event->ate_dst_part,
+ rInfo);
+ }
+ else
+ src_rInfo = dst_rInfo = rInfo;
+
/*
* Fire it. Note that the AFTER_TRIGGER_IN_PROGRESS flag is
* still set, so recursive examinations of the event list
* won't try to re-fire it.
*/
- AfterTriggerExecute(estate, event, rInfo, trigdesc, finfo, instr,
+ AfterTriggerExecute(estate, event, rInfo,
+ src_rInfo, dst_rInfo,
+ trigdesc, finfo, instr,
per_tuple_context, slot1, slot2);
/*
@@ -5767,14 +5922,35 @@ AfterTriggerPendingOnRel(Oid relid)
* Transition tuplestores are built now, rather than when events are pulled
* off of the queue because AFTER ROW triggers are allowed to select from the
* transition tables for the statement.
+ *
+ * This contains special support to queue the update events for the case where
+ * a partitioned table undergoing a cross-partition update may have foreign
+ * keys pointing into it. Normally, a partitioned table's row triggers are
+ * not fired because the leaf partition(s) which are modified as a result of
+ * the operation on the partitioned table contain the same triggers which are
+ * fired instead. But that general scheme can cause problematic behavior with
+ * foreign key triggers during cross-partition updates, which are implemented
+ * as DELETE on the source partition followed by INSERT into the destination
+ * partition. Specifically, firing DELETE triggers would lead to the wrong
+ * foreign key action to be enforced considering that the original command is
+ * UPDATE; in this case, this function is called with relinfo as the
+ * partitioned table, and src_partinfo and dst_partinfo referring to the
+ * source and target leaf partitions, respectively.
+ *
+ * is_crosspart_update is true either when a DELETE event is fired on the
+ * source partition (which is to be ignored) or an UPDATE event is fired on
+ * the root partitioned table.
* ----------
*/
static void
AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
int event, bool row_trigger,
TupleTableSlot *oldslot, TupleTableSlot *newslot,
List *recheckIndexes, Bitmapset *modifiedCols,
- TransitionCaptureState *transition_capture)
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update)
{
Relation rel = relinfo->ri_RelationDesc;
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5854,6 +6030,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
return;
}
+ /*
+ * We normally don't see partitioned tables here for row level triggers
+ * except in the special case of a cross-partition update. In that case,
+ * nodeModifyTable.c:ExecCrossPartitionUpdateForeignKey() calls here to
+ * queue an update event on the root target partitioned table, also
+ * passing the source and destination partitions and their tuples.
+ */
+ Assert(!row_trigger ||
+ rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE ||
+ (is_crosspart_update &&
+ TRIGGER_FIRED_BY_UPDATE(event) &&
+ src_partinfo != NULL && dst_partinfo != NULL));
+
/*
* Validate the event code and collect the associated tuple CTIDs.
*
@@ -5914,6 +6103,19 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
Assert(newslot != NULL);
ItemPointerCopy(&(oldslot->tts_tid), &(new_event.ate_ctid1));
ItemPointerCopy(&(newslot->tts_tid), &(new_event.ate_ctid2));
+
+ /*
+ * Also remember the OIDs of partitions to fetch these tuples
+ * out of later in AfterTriggerExecute().
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Assert(src_partinfo != NULL && dst_partinfo != NULL);
+ new_event.ate_src_part =
+ RelationGetRelid(src_partinfo->ri_RelationDesc);
+ new_event.ate_dst_part =
+ RelationGetRelid(dst_partinfo->ri_RelationDesc);
+ }
}
else
{
@@ -5938,13 +6140,53 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
}
+ /* Determine flags */
if (!(relkind == RELKIND_FOREIGN_TABLE && row_trigger))
- new_event.ate_flags = (row_trigger && event == TRIGGER_EVENT_UPDATE) ?
- AFTER_TRIGGER_2CTID : AFTER_TRIGGER_1CTID;
+ {
+ if (row_trigger && event == TRIGGER_EVENT_UPDATE)
+ {
+ if (relkind == RELKIND_PARTITIONED_TABLE)
+ new_event.ate_flags = AFTER_TRIGGER_CP_UPDATE;
+ else
+ new_event.ate_flags = AFTER_TRIGGER_2CTID;
+ }
+ else
+ new_event.ate_flags = AFTER_TRIGGER_1CTID;
+ }
+
/* else, we'll initialize ate_flags for each trigger */
tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);
+ /*
+ * Must convert/copy the source and destination partition tuples into the
+ * root partitioned table's format/slot, because the processing in the
+ * loop below expects both oldslot and newslot tuples to be in that form.
+ */
+ if (row_trigger && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ TupleTableSlot *rootslot;
+ TupleConversionMap *map;
+
+ rootslot = ExecGetTriggerOldSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(src_partinfo);
+ if (map)
+ oldslot = execute_attr_map_slot(map->attrMap,
+ oldslot,
+ rootslot);
+ else
+ oldslot = ExecCopySlot(rootslot, oldslot);
+
+ rootslot = ExecGetTriggerNewSlot(estate, relinfo);
+ map = ExecGetChildToRootMap(dst_partinfo);
+ if (map)
+ newslot = execute_attr_map_slot(map->attrMap,
+ newslot,
+ rootslot);
+ else
+ newslot = ExecCopySlot(rootslot, newslot);
+ }
+
for (i = 0; i < trigdesc->numtriggers; i++)
{
Trigger *trigger = &trigdesc->triggers[i];
@@ -5973,13 +6215,30 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
/*
* If the trigger is a foreign key enforcement trigger, there are
* certain cases where we can skip queueing the event because we can
- * tell by inspection that the FK constraint will still pass.
+ * tell by inspection that the FK constraint will still pass. There
+ * are also some cases during cross-partition updates of a partitioned
+ * table where queuing the event can be skipped.
*/
if (TRIGGER_FIRED_BY_UPDATE(event) || TRIGGER_FIRED_BY_DELETE(event))
{
switch (RI_FKey_trigger_type(trigger->tgfoid))
{
case RI_TRIGGER_PK:
+
+ /*
+ * For cross-partitioned updates of partitioned PK table,
+ * skip the event fired by the component delete on the
+ * source leaf partition unless the constraint originates
+ * in the partition itself (!tgisclone), because the
+ * update event that will be fired on the root
+ * (partitioned) target table will be used to perform the
+ * necessary foreign key enforcement action.
+ */
+ if (is_crosspart_update &&
+ TRIGGER_FIRED_BY_DELETE(event) &&
+ trigger->tgisclone)
+ continue;
+
/* Update or delete on trigger's PK table */
if (!RI_FKey_pk_upd_check_required(trigger, rel,
oldslot, newslot))
@@ -5990,8 +6249,20 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_FK:
- /* Update on trigger's FK table */
- if (!RI_FKey_fk_upd_check_required(trigger, rel,
+
+ /*
+ * Update on trigger's FK table. We can skip the update
+ * event fired on a partitioned table during a
+ * cross-partition of that table, because the insert event
+ * that is fired on the destination leaf partition would
+ * suffice to perform the necessary foreign key check.
+ * Moreover, RI_FKey_fk_upd_check_required() expects to be
+ * passed a tuple that contains system attributes, most of
+ * which are not present in the virtual slot belonging to
+ * a partitioned table.
+ */
+ if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ||
+ !RI_FKey_fk_upd_check_required(trigger, rel,
oldslot, newslot))
{
/* skip queuing this event */
@@ -6000,7 +6271,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
break;
case RI_TRIGGER_NONE:
- /* Not an FK trigger */
+
+ /*
+ * Not an FK trigger. No need to queue the update event
+ * fired during a cross-partitioned update of a
+ * partitioned table, because the same row trigger must be
+ * present in the leaf partition(s) that are affected as
+ * part of this update and the events fired on them are
+ * queued instead.
+ */
+ if (row_trigger &&
+ rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
break;
}
}
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 549d9eb696..473d2e00a2 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -44,6 +44,7 @@
#include "access/transam.h"
#include "access/xact.h"
#include "catalog/namespace.h"
+#include "catalog/partition.h"
#include "catalog/pg_publication.h"
#include "commands/matview.h"
#include "commands/trigger.h"
@@ -1279,7 +1280,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
* in es_trig_target_relations.
*/
ResultRelInfo *
-ExecGetTriggerResultRel(EState *estate, Oid relid)
+ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo)
{
ResultRelInfo *rInfo;
ListCell *l;
@@ -1330,7 +1332,7 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
InitResultRelInfo(rInfo,
rel,
0, /* dummy rangetable index */
- NULL,
+ rootRelInfo,
estate->es_instrument);
estate->es_trig_target_relations =
lappend(estate->es_trig_target_relations, rInfo);
@@ -1344,6 +1346,69 @@ ExecGetTriggerResultRel(EState *estate, Oid relid)
return rInfo;
}
+/*
+ * Return the ancestor relations of a given leaf partition result relation
+ * up to and including the query's root target relation.
+ *
+ * These work much like the ones opened by ExecGetTriggerResultRel, except
+ * that we need to keep them in a separate list.
+ *
+ * These are closed by ExecCloseResultRelations.
+ */
+List *
+ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo)
+{
+ ResultRelInfo *rootRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ Relation partRel = resultRelInfo->ri_RelationDesc;
+ Oid rootRelOid;
+
+ if (!partRel->rd_rel->relispartition)
+ elog(ERROR, "cannot find ancestors of a non-partition result relation");
+ Assert(rootRelInfo != NULL);
+ rootRelOid = RelationGetRelid(rootRelInfo->ri_RelationDesc);
+ if (resultRelInfo->ri_ancestorResultRels == NIL)
+ {
+ ListCell *lc;
+ List *oids = get_partition_ancestors(RelationGetRelid(partRel));
+ List *ancResultRels = NIL;
+
+ foreach(lc, oids)
+ {
+ Oid ancOid = lfirst_oid(lc);
+ Relation ancRel;
+ ResultRelInfo *rInfo;
+
+ /*
+ * Ignore the root ancestor here, and use ri_RootResultRelInfo
+ * (below) for it instead. Also, we stop climbing up the
+ * hierarchy when we find the table that was mentioned in the
+ * query.
+ */
+ if (ancOid == rootRelOid)
+ break;
+
+ /*
+ * All ancestors up to the root target relation must have been
+ * locked by the planner or AcquireExecutorLocks().
+ */
+ ancRel = table_open(ancOid, NoLock);
+ rInfo = makeNode(ResultRelInfo);
+
+ /* dummy rangetable index */
+ InitResultRelInfo(rInfo, ancRel, 0, NULL,
+ estate->es_instrument);
+ ancResultRels = lappend(ancResultRels, rInfo);
+ }
+ ancResultRels = lappend(ancResultRels, rootRelInfo);
+ resultRelInfo->ri_ancestorResultRels = ancResultRels;
+ }
+
+ /* We must have found some ancestor */
+ Assert(resultRelInfo->ri_ancestorResultRels != NIL);
+
+ return resultRelInfo->ri_ancestorResultRels;
+}
+
/* ----------------------------------------------------------------
* ExecPostprocessPlan
*
@@ -1443,12 +1508,29 @@ ExecCloseResultRelations(EState *estate)
/*
* close indexes of result relation(s) if any. (Rels themselves are
* closed in ExecCloseRangeTableRelations())
+ *
+ * In addition, close the stub RTs that may be in each resultrel's
+ * ri_ancestorResultRels.
*/
foreach(l, estate->es_opened_result_relations)
{
ResultRelInfo *resultRelInfo = lfirst(l);
+ ListCell *lc;
ExecCloseIndices(resultRelInfo);
+ foreach(lc, resultRelInfo->ri_ancestorResultRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+
+ /*
+ * Ancestors with RTI > 0 (should only be the root ancestor) are
+ * closed by ExecCloseRangeTableRelations.
+ */
+ if (rInfo->ri_RangeTableIndex > 0)
+ continue;
+
+ table_close(rInfo->ri_RelationDesc, NoLock);
+ }
}
/* Close any relations that have been opened by ExecGetTriggerResultRel(). */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 09f78f2244..13328141e2 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -517,8 +517,9 @@ ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo,
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
tid, NULL, slot,
- recheckIndexes, NULL);
+ recheckIndexes, NULL, false);
list_free(recheckIndexes);
}
@@ -557,7 +558,7 @@ ExecSimpleRelationDelete(ResultRelInfo *resultRelInfo,
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo,
- tid, NULL, NULL);
+ tid, NULL, NULL, false);
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index babf26810b..e01223b9bb 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -122,6 +122,12 @@ static void ExecBatchInsert(ModifyTableState *mtstate,
int numSlots,
EState *estate,
bool canSetTag);
+static void ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
+ ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot);
static bool ExecOnConflictUpdate(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer conflictTid,
@@ -635,6 +641,9 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
* slot contains the new tuple value to be stored.
*
* Returns RETURNING result if any, otherwise NULL.
+ * *inserted_tuple is the tuple that's effectively inserted;
+ * *inserted_destrel is the relation where it was inserted.
+ * These are only set on success.
*
* This may change the currently active tuple conversion map in
* mtstate->mt_transition_capture, so the callers must take care to
@@ -645,7 +654,9 @@ static TupleTableSlot *
ExecInsert(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
TupleTableSlot *slot,
- bool canSetTag)
+ bool canSetTag,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
ModifyTableState *mtstate = context->mtstate;
EState *estate = context->estate;
@@ -1008,11 +1019,14 @@ ExecInsert(ModifyTableContext *context,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture
&& mtstate->mt_transition_capture->tcs_update_new_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, NULL,
+ ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
+ NULL,
NULL,
slot,
NULL,
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1046,6 +1060,11 @@ ExecInsert(ModifyTableContext *context,
if (resultRelInfo->ri_projectReturning)
result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (inserted_tuple)
+ *inserted_tuple = slot;
+ if (insert_destrel)
+ *insert_destrel = resultRelInfo;
+
return result;
}
@@ -1160,7 +1179,7 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
*/
static void
ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
- ItemPointer tupleid, HeapTuple oldtuple)
+ ItemPointer tupleid, HeapTuple oldtuple, bool changingPart)
{
ModifyTableState *mtstate = context->mtstate;
EState *estate = context->estate;
@@ -1176,8 +1195,11 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture &&
mtstate->mt_transition_capture->tcs_update_old_table)
{
- ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple,
- NULL, NULL, mtstate->mt_transition_capture);
+ ExecARUpdateTriggers(estate, resultRelInfo,
+ NULL, NULL,
+ tupleid, oldtuple,
+ NULL, NULL, mtstate->mt_transition_capture,
+ false);
/*
* We've already captured the NEW TABLE row, so make sure any AR
@@ -1188,7 +1210,7 @@ ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
/* AFTER ROW DELETE Triggers */
ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
- ar_delete_trig_tcs);
+ ar_delete_trig_tcs, changingPart);
}
/* ----------------------------------------------------------------
@@ -1457,7 +1479,7 @@ ldelete:;
if (tupleDeleted)
*tupleDeleted = true;
- ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple);
+ ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple, changingPart);
/* Process RETURNING if present and if requested */
if (processReturning && resultRelInfo->ri_projectReturning)
@@ -1526,7 +1548,10 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
ResultRelInfo *resultRelInfo,
ItemPointer tupleid, HeapTuple oldtuple,
TupleTableSlot *slot,
- bool canSetTag, UpdateContext *updateCxt)
+ bool canSetTag,
+ UpdateContext *updateCxt,
+ TupleTableSlot **inserted_tuple,
+ ResultRelInfo **insert_destrel)
{
ModifyTableState *mtstate = context->mtstate;
EState *estate = mtstate->ps.state;
@@ -1652,7 +1677,8 @@ ExecCrossPartitionUpdate(ModifyTableContext *context,
/* Tuple routing starts from the root table. */
context->cpUpdateReturningSlot =
- ExecInsert(context, mtstate->rootResultRelInfo, slot, canSetTag);
+ ExecInsert(context, mtstate->rootResultRelInfo, slot, canSetTag,
+ inserted_tuple, insert_destrel);
/*
* Reset the transition state that may possibly have been written by
@@ -1793,6 +1819,9 @@ lreplace:;
*/
if (partition_constraint_failed)
{
+ TupleTableSlot *inserted_tuple;
+ ResultRelInfo *insert_destrel;
+
/*
* ExecCrossPartitionUpdate will first DELETE the row from the
* partition it's currently in and then insert it back into the root
@@ -1801,11 +1830,37 @@ lreplace:;
*/
if (ExecCrossPartitionUpdate(context, resultRelInfo,
tupleid, oldtuple, slot,
- canSetTag, updateCxt))
+ canSetTag, updateCxt,
+ &inserted_tuple,
+ &insert_destrel))
{
/* success! */
updateCxt->updated = true;
updateCxt->crossPartUpdate = true;
+
+ /*
+ * If the partitioned table being updated is referenced in foreign
+ * keys, queue up trigger events to check that none of them were
+ * violated. No special treatment is needed in
+ * non-cross-partition update situations, because the leaf
+ * partition's AR update triggers will take care of that. During
+ * cross-partition updates implemented as delete on the source
+ * partition followed by insert on the destination partition,
+ * AR-UPDATE triggers of the root table (that is, the table
+ * mentioned in the query) must be fired.
+ *
+ * NULL insert_destrel means that the move failed to occur, that
+ * is, the update failed, so no need to anything in that case.
+ */
+ if (insert_destrel &&
+ resultRelInfo->ri_TrigDesc &&
+ resultRelInfo->ri_TrigDesc->trig_update_after_row)
+ ExecCrossPartitionUpdateForeignKey(context,
+ resultRelInfo,
+ insert_destrel,
+ tupleid, slot,
+ inserted_tuple);
+
return TM_Ok;
}
@@ -1871,11 +1926,13 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
/* AFTER ROW UPDATE Triggers */
ExecARUpdateTriggers(context->estate, resultRelInfo,
+ NULL, NULL,
tupleid, oldtuple, slot,
recheckIndexes,
mtstate->operation == CMD_INSERT ?
mtstate->mt_oc_transition_capture :
- mtstate->mt_transition_capture);
+ mtstate->mt_transition_capture,
+ false);
/*
* Check any WITH CHECK OPTION constraints from parent views. We are
@@ -1891,6 +1948,74 @@ ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt,
slot, context->estate);
}
+/*
+ * Queues up an update event using the target root partitioned table's
+ * trigger to check that a cross-partition update hasn't broken any foreign
+ * keys pointing into it.
+ */
+static void
+ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
+ ResultRelInfo *sourcePartInfo,
+ ResultRelInfo *destPartInfo,
+ ItemPointer tupleid,
+ TupleTableSlot *oldslot,
+ TupleTableSlot *newslot)
+{
+ ListCell *lc;
+ ResultRelInfo *rootRelInfo;
+ List *ancestorRels;
+
+ rootRelInfo = sourcePartInfo->ri_RootResultRelInfo;
+ ancestorRels = ExecGetAncestorResultRels(context->estate, sourcePartInfo);
+
+ /*
+ * For any foreign keys that point directly into a non-root ancestors of
+ * the source partition, we can in theory fire an update event to enforce
+ * those constraints using their triggers, if we could tell that both the
+ * source and the destination partitions are under the same ancestor. But
+ * for now, we simply report an error that those cannot be enforced.
+ */
+ foreach(lc, ancestorRels)
+ {
+ ResultRelInfo *rInfo = lfirst(lc);
+ TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
+ bool has_noncloned_fkey = false;
+
+ /* Ignore the root ancestor, because ...?? */
+ if (rInfo == rootRelInfo)
+ continue;
+
+ if (trigdesc && trigdesc->trig_update_after_row)
+ {
+ for (int i = 0; i < trigdesc->numtriggers; i++)
+ {
+ Trigger *trig = &trigdesc->triggers[i];
+
+ if (!trig->tgisclone &&
+ RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK)
+ {
+ has_noncloned_fkey = true;
+ break;
+ }
+ }
+ }
+
+ if (has_noncloned_fkey)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key"),
+ errdetail("A foreign key points to ancestor \"%s\", but not the root ancestor \"%s\".",
+ RelationGetRelationName(rInfo->ri_RelationDesc),
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc)),
+ errhint("Consider defining the foreign key on \"%s\".",
+ RelationGetRelationName(rootRelInfo->ri_RelationDesc))));
+ }
+
+ /* Perform the root table's triggers. */
+ ExecARUpdateTriggers(context->estate,
+ rootRelInfo, sourcePartInfo, destPartInfo,
+ tupleid, NULL, newslot, NIL, NULL, true);
+}
/* ----------------------------------------------------------------
* ExecUpdate
@@ -2745,7 +2870,7 @@ ExecModifyTable(PlanState *pstate)
ExecInitInsertProjection(node, resultRelInfo);
slot = ExecGetInsertNewTuple(resultRelInfo, planSlot);
slot = ExecInsert(&context, resultRelInfo, slot,
- node->canSetTag);
+ node->canSetTag, NULL, NULL);
break;
case CMD_UPDATE:
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index c95cd32402..01d4c22cfc 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -1261,6 +1261,12 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel,
TransactionId xmin;
bool isnull;
+ /*
+ * AfterTriggerSaveEvent() handles things such that this function is never
+ * called for partitioned tables.
+ */
+ Assert(fk_rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE);
+
riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false);
ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index e1271420e5..66bf6c16e3 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -214,7 +214,8 @@ extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple);
@@ -231,11 +232,14 @@ extern bool ExecBRUpdateTriggers(EState *estate,
TupleTableSlot *slot);
extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
+ ResultRelInfo *src_partinfo,
+ ResultRelInfo *dst_partinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *slot,
List *recheckIndexes,
- TransitionCaptureState *transition_capture);
+ TransitionCaptureState *transition_capture,
+ bool is_crosspart_update);
extern bool ExecIRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
HeapTuple trigtuple,
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 344399f6a8..82925b4b63 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -203,7 +203,9 @@ extern void InitResultRelInfo(ResultRelInfo *resultRelInfo,
Index resultRelationIndex,
ResultRelInfo *partition_root_rri,
int instrument_options);
-extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
+extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid,
+ ResultRelInfo *rootRelInfo);
+extern List *ExecGetAncestorResultRels(EState *estate, ResultRelInfo *resultRelInfo);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern bool ExecPartitionCheck(ResultRelInfo *resultRelInfo,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index dd95dc40c7..44dd73fc80 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -530,6 +530,12 @@ typedef struct ResultRelInfo
/* for use by copyfrom.c when performing multi-inserts */
struct CopyMultiInsertBuffer *ri_CopyMultiInsertBuffer;
+
+ /*
+ * Used when a leaf partition is involved in a cross-partition update of
+ * one of its ancestors; see ExecCrossPartitionUpdateForeignKey().
+ */
+ List *ri_ancestorResultRels;
} ResultRelInfo;
/* ----------------
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 4c5274983d..da26f083bc 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -2556,7 +2556,7 @@ DELETE FROM pk WHERE a = 20;
ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
DETAIL: Key (a)=(20) is still referenced from table "fk".
UPDATE pk SET a = 90 WHERE a = 30;
-ERROR: update or delete on table "pk11" violates foreign key constraint "fk_a_fkey2" on table "fk"
+ERROR: update or delete on table "pk" violates foreign key constraint "fk_a_fkey" on table "fk"
DETAIL: Key (a)=(30) is still referenced from table "fk".
SELECT tableoid::regclass, * FROM fk;
tableoid | a
@@ -2625,15 +2625,213 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+ERROR: update or delete on table "tbl1" violates foreign key constraint "tbl2_f1_fkey" on table "tbl2"
+DETAIL: Key (f1)=(0) is still referenced from table "tbl2".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl5_f1_fkey" on table "tbl5"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl5".
+COMMIT;
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+ERROR: update or delete on table "tbl3" violates foreign key constraint "tbl4_f1_fkey" on table "tbl4"
+DETAIL: Key (f1)=(-2) is still referenced from table "tbl4".
DROP SCHEMA fkpart10 CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 5 other objects
DETAIL: drop cascades to table fkpart10.tbl1
drop cascades to table fkpart10.tbl2
+drop cascades to table fkpart10.tbl3
+drop cascades to table fkpart10.tbl4
+drop cascades to table fkpart10.tbl5
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk12 | 2 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 2
+ fkpart11.fk | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 2
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 2
+ fkpart11.fk_another | 4
+(2 rows)
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+ERROR: cannot move tuple across partitions when a non-root ancestor of the source partition is directly referenced in a foreign key
+DETAIL: A foreign key points to ancestor "pk1", but not the root ancestor "pk".
+HINT: Consider defining the foreign key on "pk".
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+ tableoid | a | b
+---------------+---+-----
+ fkpart11.pk11 | 1 | xxx
+ fkpart11.pk3 | 4 | yyy
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+-------------+---
+ fkpart11.fk | 1
+(1 row)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+ tableoid | a
+--------------+---
+ fkpart11.fk1 | 1
+ fkpart11.fk3 | 4
+(2 rows)
+
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+ tableoid | a
+---------------------+---
+ fkpart11.fk_another | 4
+ fkpart11.fk_another | 1
+(2 rows)
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+ tableoid | a
+----------+---
+(0 rows)
+
+DROP TABLE fkpart11.fk;
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+NOTICE: TABLE: pk3, OP: DELETE, OLD: (4,yyy), NEW: <NULL>
+NOTICE: TABLE: pk2, OP: INSERT, OLD: <NULL>, NEW: (3,yyy)
+NOTICE: TABLE: fk3, OP: DELETE, OLD: (4), NEW: <NULL>
+NOTICE: TABLE: fk2, OP: INSERT, OLD: <NULL>, NEW: (3)
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+NOTICE: TABLE: pk12, OP: DELETE, OLD: (xxx,2), NEW: <NULL>
+NOTICE: TABLE: pk11, OP: INSERT, OLD: <NULL>, NEW: (xxx,1)
+NOTICE: TABLE: fk1, OP: UPDATE, OLD: (2), NEW: (1)
+DROP SCHEMA fkpart11 CASCADE;
+NOTICE: drop cascades to 4 other objects
+DETAIL: drop cascades to table fkpart11.pk
+drop cascades to table fkpart11.fk_parted
+drop cascades to table fkpart11.fk_another
+drop cascades to function fkpart11.print_row()
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index fa781b6e32..725a59a525 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1871,12 +1871,145 @@ CREATE SCHEMA fkpart10
CREATE TABLE tbl1(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
CREATE TABLE tbl1_p1 PARTITION OF tbl1 FOR VALUES FROM (minvalue) TO (1)
CREATE TABLE tbl1_p2 PARTITION OF tbl1 FOR VALUES FROM (1) TO (maxvalue)
- CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED);
+ CREATE TABLE tbl2(f1 int REFERENCES tbl1 DEFERRABLE INITIALLY DEFERRED)
+ CREATE TABLE tbl3(f1 int PRIMARY KEY) PARTITION BY RANGE(f1)
+ CREATE TABLE tbl3_p1 PARTITION OF tbl3 FOR VALUES FROM (minvalue) TO (1)
+ CREATE TABLE tbl3_p2 PARTITION OF tbl3 FOR VALUES FROM (1) TO (maxvalue)
+ CREATE TABLE tbl4(f1 int REFERENCES tbl3 DEFERRABLE INITIALLY DEFERRED);
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
INSERT INTO fkpart10.tbl2 VALUES (0), (1);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1), (0);
+INSERT INTO fkpart10.tbl4 VALUES (-2), (-1);
BEGIN;
DELETE FROM fkpart10.tbl1 WHERE f1 = 0;
UPDATE fkpart10.tbl1 SET f1 = 2 WHERE f1 = 1;
INSERT INTO fkpart10.tbl1 VALUES (0), (1);
COMMIT;
+
+-- test that cross-partition updates correctly enforces the foreign key
+-- restriction (specifically testing INITIAILLY DEFERRED)
+BEGIN;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+INSERT INTO fkpart10.tbl1 VALUES (4);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl3 SET f1 = f1 + 3;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+COMMIT;
+
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -1;
+UPDATE fkpart10.tbl1 SET f1 = 3 WHERE f1 = 0;
+INSERT INTO fkpart10.tbl1 VALUES (0);
+INSERT INTO fkpart10.tbl3 VALUES (-2), (-1);
+COMMIT;
+
+-- test where the updated table now has both an IMMEDIATE and a DEFERRED
+-- constraint pointing into it
+CREATE TABLE fkpart10.tbl5(f1 int REFERENCES fkpart10.tbl3);
+INSERT INTO fkpart10.tbl5 VALUES (-2), (-1);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
+-- Now test where the row referenced from the table with an IMMEDIATE
+-- constraint stays in place, while those referenced from the table with a
+-- DEFERRED constraint don't.
+DELETE FROM fkpart10.tbl5;
+INSERT INTO fkpart10.tbl5 VALUES (0);
+BEGIN;
+UPDATE fkpart10.tbl3 SET f1 = f1 * -3;
+COMMIT;
+
DROP SCHEMA fkpart10 CASCADE;
+
+-- verify foreign keys are enforced during cross-partition updates,
+-- especially on the PK side
+CREATE SCHEMA fkpart11
+ CREATE TABLE pk (a INT PRIMARY KEY, b text) PARTITION BY LIST (a)
+ CREATE TABLE fk (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE fk_parted (
+ a INT PRIMARY KEY,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES pk(a) ON UPDATE CASCADE ON DELETE CASCADE
+ ) PARTITION BY LIST (a)
+ CREATE TABLE fk_another (
+ a INT,
+ CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fk_parted (a) ON UPDATE CASCADE ON DELETE CASCADE
+ )
+ CREATE TABLE pk1 PARTITION OF pk FOR VALUES IN (1, 2) PARTITION BY LIST (a)
+ CREATE TABLE pk2 PARTITION OF pk FOR VALUES IN (3)
+ CREATE TABLE pk3 PARTITION OF pk FOR VALUES IN (4)
+ CREATE TABLE fk1 PARTITION OF fk_parted FOR VALUES IN (1, 2)
+ CREATE TABLE fk2 PARTITION OF fk_parted FOR VALUES IN (3)
+ CREATE TABLE fk3 PARTITION OF fk_parted FOR VALUES IN (4);
+CREATE TABLE fkpart11.pk11 (b text, a int NOT NULL);
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk11 FOR VALUES IN (1);
+CREATE TABLE fkpart11.pk12 (b text, c int, a int NOT NULL);
+ALTER TABLE fkpart11.pk12 DROP c;
+ALTER TABLE fkpart11.pk1 ATTACH PARTITION fkpart11.pk12 FOR VALUES IN (2);
+INSERT INTO fkpart11.pk VALUES (1, 'xxx'), (3, 'yyy');
+INSERT INTO fkpart11.fk VALUES (1), (3);
+INSERT INTO fkpart11.fk_parted VALUES (1), (3);
+INSERT INTO fkpart11.fk_another VALUES (1), (3);
+-- moves 2 rows from one leaf partition to another, with both updates being
+-- cascaded to fk and fk_parted. Updates of fk_parted, of which one is
+-- cross-partition (3 -> 4), are further cascaded to fk_another.
+UPDATE fkpart11.pk SET a = a + 1 RETURNING tableoid::pg_catalog.regclass, *;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- let's try with the foreign key pointing at tables in the partition tree
+-- that are not the same as the query's target table
+
+-- 1. foreign key pointing into a non-root ancestor
+--
+-- A cross-partition update on the root table will fail, because we currently
+-- can't enforce the foreign keys pointing into a non-leaf partition
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+DELETE FROM fkpart11.fk WHERE a = 4;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk1 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+UPDATE fkpart11.pk SET a = a - 1;
+-- it's okay though if the non-leaf partition is updated directly
+UPDATE fkpart11.pk1 SET a = a - 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.pk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_parted;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk_another;
+
+-- 2. foreign key pointing into a single leaf partition
+--
+-- A cross-partition update that deletes from the pointed-to leaf partition
+-- is allowed to succeed
+ALTER TABLE fkpart11.fk DROP CONSTRAINT fkey;
+ALTER TABLE fkpart11.fk ADD CONSTRAINT fkey FOREIGN KEY (a) REFERENCES fkpart11.pk11 (a) ON UPDATE CASCADE ON DELETE CASCADE;
+-- will delete (1) from p11 which is cascaded to fk
+UPDATE fkpart11.pk SET a = a + 1 WHERE a = 1;
+SELECT tableoid::pg_catalog.regclass, * FROM fkpart11.fk;
+DROP TABLE fkpart11.fk;
+
+-- check that regular and deferrable AR triggers on the PK tables
+-- still work as expected
+CREATE FUNCTION fkpart11.print_row () RETURNS TRIGGER LANGUAGE plpgsql AS $$
+ BEGIN
+ RAISE NOTICE 'TABLE: %, OP: %, OLD: %, NEW: %', TG_RELNAME, TG_OP, OLD, NEW;
+ RETURN NULL;
+ END;
+$$;
+CREATE TRIGGER trig_upd_pk AFTER UPDATE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_del_pk AFTER DELETE ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE TRIGGER trig_ins_pk AFTER INSERT ON fkpart11.pk FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_upd_fk_parted AFTER UPDATE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_del_fk_parted AFTER DELETE ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+CREATE CONSTRAINT TRIGGER trig_ins_fk_parted AFTER INSERT ON fkpart11.fk_parted INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION fkpart11.print_row();
+UPDATE fkpart11.pk SET a = 3 WHERE a = 4;
+UPDATE fkpart11.pk SET a = 1 WHERE a = 2;
+
+DROP SCHEMA fkpart11 CASCADE;
--
2.30.2
On Sun, Mar 20, 2022 at 5:13 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Mar-18, Zhihong Yu wrote:
+#define AFTER_TRIGGER_OFFSET 0x07FFFFFF /* must be low-order bits */ +#define AFTER_TRIGGER_DONE 0x80000000 +#define AFTER_TRIGGER_IN_PROGRESS 0x40000000Is it better if the order of AFTER_TRIGGER_DONE
and AFTER_TRIGGER_IN_PROGRESS is swapped (for the ordinal values to be
sequential) ?They *are* sequential -- See
/messages/by-id/202201172215.2tse3vjjgi2b@alvherre.pgsql+#define AFTER_TRIGGER_CP_UPDATE 0x08000000
It would be better to add a comment for this constant, explaining what CP
means (cross partition).Sure.
Thanks.
+ if (!partRel->rd_rel->relispartition) + elog(ERROR, "cannot find ancestors of a non-partition result relation");It would be better to include the relation name in the error message.
I don't think it matters. We don't really expect to hit this.
I tend to think maybe showing at least the OID in the error message
doesn't hurt, but maybe we don't need to.
+ /* Ignore the root ancestor, because ...?? */
Please fill out the remainder of the comment.
I actually would like to know what's the rationale for this myself.
Amit?
Ah, it's just that the ancstorRels list contains *all* ancestors,
including the root one, whose triggers will actually be fired to
enforce its foreign key. The code below the line of code that this
comment is for is to check *non-root* ancestor's triggers to spot any
that look like they enforce foreign keys to flag them as
unenforceable.
I've fixed the comment as:
- /* Ignore the root ancestor, because ...?? */
+ /* Root ancestor's triggers will be processed. */
+ if (!trig->tgisclone && + RI_FKey_trigger_type(trig->tgfoid) == RI_TRIGGER_PK) + { + has_noncloned_fkey = true;The variable says fkey, but the constant is not RI_TRIGGER_FK. Maybe add a
comment explaining why.Well, the constant is about the trigger *function*, not about any
constraint. This code is testing "is this a noncloned trigger, and does
that trigger use an FK-related function?" If you have a favorite
comment to include, I'm all ears.
A description of what we're looking for with this code is in the
comment above the loop:
/*
* For any foreign keys that point directly into a non-root ancestors of
* the source partition,...
So finding triggers in those non-root ancestors whose function is
RI_TRIGGER_PK tells us that those relations have foreign keys pointing
into it or that it is the PK table in that relationship. Other than
the comment, the code itself seems self-documenting with regard to
what's being done (given the function/macro/variable naming), so maybe
we're better off without additional commentary here.
I've attached a delta patch on v16 for the above comment and a couple
of other changes.
--
Amit Langote
EDB: http://www.enterprisedb.com
Attachments:
v16_delta.diffapplication/octet-stream; name=v16_delta.diffDownload
diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml
index 3ba13010e7..3a0285df79 100644
--- a/doc/src/sgml/ref/update.sgml
+++ b/doc/src/sgml/ref/update.sgml
@@ -319,9 +319,9 @@ UPDATE <replaceable class="parameter">count</replaceable>
<para>
An attempt of moving a row from one partition to another will fail if a
- foreign key is found to directly reference a non-root partitioned table
- in the partition tree, unless that table is also directly mentioned
- in the <command>UPDATE</command>query.
+ foreign key is found to directly reference an ancestor of the source
+ partition that is not the same as the ancestor that's mentioned in the
+ <command>UPDATE</command> query.
</para>
</refsect1>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 0adecdea22..5fa395fcc2 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -3542,7 +3542,7 @@ typedef SetConstraintStateData *SetConstraintState;
* Per-trigger-event data
*
* The actual per-event data, AfterTriggerEventData, includes DONE/IN_PROGRESS
- * status bits, up to two tuple CTIDs, and optionally two OIDs of partitions
+ * status bits, up to two tuple CTIDs, and optionally two OIDs of partitions.
* Each event record also has an associated AfterTriggerSharedData that is
* shared across all instances of similar events within a "chunk".
*
@@ -3555,10 +3555,10 @@ typedef SetConstraintStateData *SetConstraintState;
* tuple(s). This permits storing tuples once regardless of the number of
* row-level triggers on a foreign table.
*
- * When updates move tuples in partitioned tables to different partitions,
+ * When updates of a partitioned table causes rows to move between partitions,
* the OIDs of both partitions are stored too, so that the tuples can be
* fetched; such entries are marked AFTER_TRIGGER_CP_UPDATE (for "cross-
- * partition").
+ * partition update").
*
* Note that we need triggers on foreign tables to be fired in exactly the
* order they were queued, so that the tuples come out of the tuplestore in
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index de46315405..c822b5b744 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1981,7 +1981,7 @@ ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
TriggerDesc *trigdesc = rInfo->ri_TrigDesc;
bool has_noncloned_fkey = false;
- /* Ignore the root ancestor, because ...?? */
+ /* Root ancestor's triggers will be processed. */
if (rInfo == rootRelInfo)
continue;
On 2022-Mar-20, Amit Langote wrote:
On Sun, Mar 20, 2022 at 5:13 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Mar-18, Zhihong Yu wrote:
+ if (!partRel->rd_rel->relispartition) + elog(ERROR, "cannot find ancestors of a non-partition result relation");It would be better to include the relation name in the error message.
I don't think it matters. We don't really expect to hit this.
I tend to think maybe showing at least the OID in the error message
doesn't hurt, but maybe we don't need to.
Since we don't even know of a situation in which this error message
would be raised, I'm hardly bothered by failing to print the OID. If
any users complain, we can add more detail.
I've fixed the comment as:
- /* Ignore the root ancestor, because ...?? */ + /* Root ancestor's triggers will be processed. */
Okay, included that.
A description of what we're looking for with this code is in the
comment above the loop:/*
* For any foreign keys that point directly into a non-root ancestors of
* the source partition,...So finding triggers in those non-root ancestors whose function is
RI_TRIGGER_PK tells us that those relations have foreign keys pointing
into it or that it is the PK table in that relationship. Other than
the comment, the code itself seems self-documenting with regard to
what's being done (given the function/macro/variable naming), so maybe
we're better off without additional commentary here.
Yeah, WFM.
I've attached a delta patch on v16 for the above comment and a couple
of other changes.
Merged that in, and pushed. I made a couple of wording changes in
comments here and there as well.
I lament the fact that this fix is not going to hit Postgres 12-14, but
ratio of effort to reward seems a bit too high. I think we could
backpatch the two involved commits if someone is motivated enough to
verify everything and come up with solutions for the necessary ABI
changes.
Thank you, Amit, for your perseverance in getting this bug fixed!
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
"No hay hombre que no aspire a la plenitud, es decir,
la suma de experiencias de que un hombre es capaz"
Hi Alvaro,
On Mon, Mar 21, 2022 at 2:58 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Mar-20, Amit Langote wrote:
On Sun, Mar 20, 2022 at 5:13 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2022-Mar-18, Zhihong Yu wrote:
+ if (!partRel->rd_rel->relispartition) + elog(ERROR, "cannot find ancestors of a non-partition result relation");It would be better to include the relation name in the error message.
I don't think it matters. We don't really expect to hit this.
I tend to think maybe showing at least the OID in the error message
doesn't hurt, but maybe we don't need to.Since we don't even know of a situation in which this error message
would be raised, I'm hardly bothered by failing to print the OID. If
any users complain, we can add more detail.
Sure.
I lament the fact that this fix is not going to hit Postgres 12-14, but
ratio of effort to reward seems a bit too high. I think we could
backpatch the two involved commits if someone is motivated enough to
verify everything and come up with solutions for the necessary ABI
changes.Thank you, Amit, for your perseverance in getting this bug fixed!
Thanks a lot for taking the time to review and commit.
--
Amit Langote
EDB: http://www.enterprisedb.com