Commit 86f57594 authored by Alvaro Herrera's avatar Alvaro Herrera

Allow FOR EACH ROW triggers on partitioned tables

Previously, FOR EACH ROW triggers were not allowed in partitioned
tables.  Now we allow AFTER triggers on them, and on trigger creation we
cascade to create an identical trigger in each partition.  We also clone
the triggers to each partition that is created or attached later.

This means that deferred unique keys are allowed on partitioned tables,
too.

Author: Álvaro Herrera
Reviewed-by: Peter Eisentraut, Simon Riggs, Amit Langote, Robert Haas,
	Thomas Munro
Discussion: https://postgr.es/m/20171229225319.ajltgss2ojkfd3kp@alvherre.pgsql
parent 5700aa13
......@@ -2254,6 +2254,14 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
key, foreign key, or exclusion constraint; else 0</entry>
</row>
<row>
<entry><structfield>conparentid</structfield></entry>
<entry><type>oid</type></entry>
<entry><literal><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link>.oid</literal></entry>
<entry>The corresponding constraint in the parent partitioned table,
if this is a constraint in a partition; else 0</entry>
</row>
<row>
<entry><structfield>confrelid</structfield></entry>
<entry><type>oid</type></entry>
......
......@@ -512,6 +512,13 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
the ones that are fired.
</para>
<para>
Creating a row-level trigger on a partitioned table will cause identical
triggers to be created in all its existing partitions; and any partitions
created or attached later will contain an identical trigger, too.
Triggers on partitioned tables may only be <literal>AFTER</literal>.
</para>
<para>
Modifying a partitioned table or a table with inheritance children fires
statement-level triggers attached to the explicitly named table, but not
......
......@@ -2121,6 +2121,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
false, /* Is Deferrable */
false, /* Is Deferred */
is_validated,
InvalidOid, /* no parent constraint */
RelationGetRelid(rel), /* relation */
attNos, /* attrs in the constraint */
keycount, /* # attrs in the constraint */
......
......@@ -1282,6 +1282,7 @@ index_constraint_create(Relation heapRelation,
deferrable,
initdeferred,
true,
parentConstraintId,
RelationGetRelid(heapRelation),
indexInfo->ii_KeyAttrNumbers,
indexInfo->ii_NumIndexAttrs,
......@@ -1360,7 +1361,8 @@ index_constraint_create(Relation heapRelation,
trigger->constrrel = NULL;
(void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
InvalidOid, conOid, indexRelationId, true);
InvalidOid, conOid, indexRelationId, InvalidOid,
InvalidOid, NULL, true, false);
}
/*
......
......@@ -52,6 +52,7 @@ CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
int constraintNKeys,
......@@ -170,6 +171,7 @@ CreateConstraintEntry(const char *constraintName,
values[Anum_pg_constraint_conrelid - 1] = ObjectIdGetDatum(relId);
values[Anum_pg_constraint_contypid - 1] = ObjectIdGetDatum(domainId);
values[Anum_pg_constraint_conindid - 1] = ObjectIdGetDatum(indexRelId);
values[Anum_pg_constraint_conparentid - 1] = ObjectIdGetDatum(parentConstrId);
values[Anum_pg_constraint_confrelid - 1] = ObjectIdGetDatum(foreignRelId);
values[Anum_pg_constraint_confupdtype - 1] = CharGetDatum(foreignUpdateType);
values[Anum_pg_constraint_confdeltype - 1] = CharGetDatum(foreignDeleteType);
......@@ -772,6 +774,7 @@ ConstraintSetParentConstraint(Oid childConstrId, Oid parentConstrId)
constrForm = (Form_pg_constraint) GETSTRUCT(newtup);
constrForm->conislocal = false;
constrForm->coninhcount++;
constrForm->conparentid = parentConstrId;
CatalogTupleUpdate(constrRel, &tuple->t_self, newtup);
ReleaseSysCache(tuple);
......
......@@ -487,6 +487,7 @@ static void ValidatePartitionConstraints(List **wqueue, Relation scanrel,
List *scanrel_children,
List *partConstraint,
bool validate_default);
static void CloneRowTriggersToPartition(Relation parent, Relation partition);
static ObjectAddress ATExecDetachPartition(Relation rel, RangeVar *name);
static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation rel,
RangeVar *name);
......@@ -906,9 +907,11 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
}
/*
* If we're creating a partition, create now all the indexes defined in
* the parent. We can't do it earlier, because DefineIndex wants to know
* the partition key which we just stored.
* If we're creating a partition, create now all the indexes and triggers
* defined in the parent.
*
* We can't do it earlier, because DefineIndex wants to know the partition
* key which we just stored.
*/
if (stmt->partbound)
{
......@@ -949,6 +952,14 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
}
list_free(idxlist);
/*
* If there are any row-level triggers, clone them to the new
* partition.
*/
if (parent->trigdesc != NULL)
CloneRowTriggersToPartition(parent, rel);
heap_close(parent, NoLock);
}
......@@ -7491,6 +7502,7 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
fkconstraint->deferrable,
fkconstraint->initdeferred,
fkconstraint->initially_valid,
InvalidOid, /* no parent constraint */
RelationGetRelid(rel),
fkattnum,
numfks,
......@@ -8445,7 +8457,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->args = NIL;
(void) CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid, constraintOid,
indexOid, true);
indexOid, InvalidOid, InvalidOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
......@@ -8519,7 +8531,7 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->args = NIL;
(void) CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid, constraintOid,
indexOid, true);
indexOid, InvalidOid, InvalidOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
......@@ -8574,7 +8586,7 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
fk_trigger->args = NIL;
(void) CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid, constraintOid,
indexOid, true);
indexOid, InvalidOid, InvalidOid, NULL, true, false);
/* Make changes-so-far visible */
CommandCounterIncrement();
......@@ -11114,7 +11126,7 @@ static void
ATExecEnableDisableTrigger(Relation rel, const char *trigname,
char fires_when, bool skip_system, LOCKMODE lockmode)
{
EnableDisableTrigger(rel, trigname, fires_when, skip_system);
EnableDisableTrigger(rel, trigname, fires_when, skip_system, lockmode);
}
/*
......@@ -14031,6 +14043,9 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd)
/* Ensure there exists a correct set of indexes in the partition. */
AttachPartitionEnsureIndexes(rel, attachrel);
/* and triggers */
CloneRowTriggersToPartition(rel, attachrel);
/*
* Generate partition constraint from the partition bound specification.
* If the parent itself is a partition, make sure to include its
......@@ -14254,6 +14269,127 @@ AttachPartitionEnsureIndexes(Relation rel, Relation attachrel)
MemoryContextDelete(cxt);
}
/*
* CloneRowTriggersToPartition
* subroutine for ATExecAttachPartition/DefineRelation to create row
* triggers on partitions
*/
static void
CloneRowTriggersToPartition(Relation parent, Relation partition)
{
Relation pg_trigger;
ScanKeyData key;
SysScanDesc scan;
HeapTuple tuple;
MemoryContext oldcxt,
perTupCxt;
ScanKeyInit(&key, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
F_OIDEQ, ObjectIdGetDatum(RelationGetRelid(parent)));
pg_trigger = heap_open(TriggerRelationId, RowExclusiveLock);
scan = systable_beginscan(pg_trigger, TriggerRelidNameIndexId,
true, NULL, 1, &key);
perTupCxt = AllocSetContextCreate(CurrentMemoryContext,
"clone trig", ALLOCSET_SMALL_SIZES);
oldcxt = MemoryContextSwitchTo(perTupCxt);
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
{
Form_pg_trigger trigForm;
CreateTrigStmt *trigStmt;
Node *qual = NULL;
Datum value;
bool isnull;
List *cols = NIL;
trigForm = (Form_pg_trigger) GETSTRUCT(tuple);
/*
* Ignore statement-level triggers; those are not cloned.
*/
if (!TRIGGER_FOR_ROW(trigForm->tgtype))
continue;
/*
* Complain if we find an unexpected trigger type.
*/
if (!TRIGGER_FOR_AFTER(trigForm->tgtype))
elog(ERROR, "unexpected trigger \"%s\" found",
NameStr(trigForm->tgname));
/*
* If there is a WHEN clause, generate a 'cooked' version of it that's
* appropriate for the partition.
*/
value = heap_getattr(tuple, Anum_pg_trigger_tgqual,
RelationGetDescr(pg_trigger), &isnull);
if (!isnull)
{
bool found_whole_row;
qual = stringToNode(TextDatumGetCString(value));
qual = (Node *) map_partition_varattnos((List *) qual, PRS2_OLD_VARNO,
partition, parent,
&found_whole_row);
if (found_whole_row)
elog(ERROR, "unexpected whole-row reference found in trigger WHEN clause");
qual = (Node *) map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
partition, parent,
&found_whole_row);
if (found_whole_row)
elog(ERROR, "unexpected whole-row reference found in trigger WHEN clause");
}
/*
* If there is a column list, transform it to a list of column names.
* Note we don't need to map this list in any way ...
*/
if (trigForm->tgattr.dim1 > 0)
{
int i;
for (i = 0; i < trigForm->tgattr.dim1; i++)
{
Form_pg_attribute col;
col = TupleDescAttr(parent->rd_att,
trigForm->tgattr.values[i] - 1);
cols = lappend(cols, makeString(NameStr(col->attname)));
}
}
trigStmt = makeNode(CreateTrigStmt);
trigStmt->trigname = NameStr(trigForm->tgname);
trigStmt->relation = NULL;
trigStmt->funcname = NULL; /* passed separately */
trigStmt->args = NULL; /* passed separately */
trigStmt->row = true;
trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
trigStmt->columns = cols;
trigStmt->whenClause = NULL; /* passed separately */
trigStmt->isconstraint = OidIsValid(trigForm->tgconstraint);
trigStmt->transitionRels = NIL; /* not supported at present */
trigStmt->deferrable = trigForm->tgdeferrable;
trigStmt->initdeferred = trigForm->tginitdeferred;
trigStmt->constrrel = NULL; /* passed separately */
CreateTrigger(trigStmt, NULL, RelationGetRelid(partition),
trigForm->tgconstrrelid, InvalidOid, InvalidOid,
trigForm->tgfoid, HeapTupleGetOid(tuple), qual,
false, true);
MemoryContextReset(perTupCxt);
}
MemoryContextSwitchTo(oldcxt);
MemoryContextDelete(perTupCxt);
systable_endscan(scan);
heap_close(pg_trigger, RowExclusiveLock);
}
/*
* ALTER TABLE DETACH PARTITION
*
......
This diff is collapsed.
......@@ -3153,6 +3153,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
false, /* Is Deferrable */
false, /* Is Deferred */
!constr->skip_validation, /* Is Validated */
InvalidOid, /* no parent constraint */
InvalidOid, /* not a relation constraint */
NULL,
0,
......
......@@ -1492,7 +1492,8 @@ ProcessUtilitySlow(ParseState *pstate,
case T_CreateTrigStmt:
address = CreateTrigger((CreateTrigStmt *) parsetree,
queryString, InvalidOid, InvalidOid,
InvalidOid, InvalidOid, false);
InvalidOid, InvalidOid, InvalidOid,
InvalidOid, NULL, false, false);
break;
case T_CreatePLangStmt:
......
......@@ -53,6 +53,6 @@
*/
/* yyyymmddN */
#define CATALOG_VERSION_NO 201803213
#define CATALOG_VERSION_NO 201803231
#endif
......@@ -128,6 +128,8 @@ DECLARE_INDEX(pg_constraint_contypid_index, 2666, on pg_constraint using btree(c
#define ConstraintTypidIndexId 2666
DECLARE_UNIQUE_INDEX(pg_constraint_oid_index, 2667, on pg_constraint using btree(oid oid_ops));
#define ConstraintOidIndexId 2667
DECLARE_INDEX(pg_constraint_conparentid_index, 2579, on pg_constraint using btree(conparentid oid_ops));
#define ConstraintParentIndexId 2579
DECLARE_UNIQUE_INDEX(pg_conversion_default_index, 2668, on pg_conversion using btree(connamespace oid_ops, conforencoding int4_ops, contoencoding int4_ops, oid oid_ops));
#define ConversionDefaultIndexId 2668
......
......@@ -72,6 +72,12 @@ CATALOG(pg_constraint,2606)
*/
Oid conindid; /* index supporting this constraint */
/*
* If this constraint is on a partition inherited from a partitioned
* table, this is the OID of the corresponding constraint in the parent.
*/
Oid conparentid;
/*
* These fields, plus confkey, are only meaningful for a foreign-key
* constraint. Otherwise confrelid is 0 and the char fields are spaces.
......@@ -150,7 +156,7 @@ typedef FormData_pg_constraint *Form_pg_constraint;
* compiler constants for pg_constraint
* ----------------
*/
#define Natts_pg_constraint 24
#define Natts_pg_constraint 25
#define Anum_pg_constraint_conname 1
#define Anum_pg_constraint_connamespace 2
#define Anum_pg_constraint_contype 3
......@@ -160,21 +166,22 @@ typedef FormData_pg_constraint *Form_pg_constraint;
#define Anum_pg_constraint_conrelid 7
#define Anum_pg_constraint_contypid 8
#define Anum_pg_constraint_conindid 9
#define Anum_pg_constraint_confrelid 10
#define Anum_pg_constraint_confupdtype 11
#define Anum_pg_constraint_confdeltype 12
#define Anum_pg_constraint_confmatchtype 13
#define Anum_pg_constraint_conislocal 14
#define Anum_pg_constraint_coninhcount 15
#define Anum_pg_constraint_connoinherit 16
#define Anum_pg_constraint_conkey 17
#define Anum_pg_constraint_confkey 18
#define Anum_pg_constraint_conpfeqop 19
#define Anum_pg_constraint_conppeqop 20
#define Anum_pg_constraint_conffeqop 21
#define Anum_pg_constraint_conexclop 22
#define Anum_pg_constraint_conbin 23
#define Anum_pg_constraint_consrc 24
#define Anum_pg_constraint_conparentid 10
#define Anum_pg_constraint_confrelid 11
#define Anum_pg_constraint_confupdtype 12
#define Anum_pg_constraint_confdeltype 13
#define Anum_pg_constraint_confmatchtype 14
#define Anum_pg_constraint_conislocal 15
#define Anum_pg_constraint_coninhcount 16
#define Anum_pg_constraint_connoinherit 17
#define Anum_pg_constraint_conkey 18
#define Anum_pg_constraint_confkey 19
#define Anum_pg_constraint_conpfeqop 20
#define Anum_pg_constraint_conppeqop 21
#define Anum_pg_constraint_conffeqop 22
#define Anum_pg_constraint_conexclop 23
#define Anum_pg_constraint_conbin 24
#define Anum_pg_constraint_consrc 25
/* ----------------
* initial contents of pg_constraint
......
......@@ -33,6 +33,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool isDeferrable,
bool isDeferred,
bool isValidated,
Oid parentConstrId,
Oid relId,
const int16 *constraintKey,
int constraintNKeys,
......
......@@ -159,7 +159,8 @@ extern PGDLLIMPORT int SessionReplicationRole;
extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
bool isInternal);
Oid funcoid, Oid parentTriggerOid, Node *whenClause,
bool isInternal, bool in_partition);
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
......@@ -167,7 +168,7 @@ extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
extern ObjectAddress renametrig(RenameStmt *stmt);
extern void EnableDisableTrigger(Relation rel, const char *tgname,
char fires_when, bool skip_system);
char fires_when, bool skip_system, LOCKMODE lockmode);
extern void RelationBuildTriggers(Relation relation);
......
......@@ -369,6 +369,14 @@ WHERE conindid != 0 AND
------+----------
(0 rows)
SELECT ctid, conparentid
FROM pg_catalog.pg_constraint fk
WHERE conparentid != 0 AND
NOT EXISTS(SELECT 1 FROM pg_catalog.pg_constraint pk WHERE pk.oid = fk.conparentid);
ctid | conparentid
------+-------------
(0 rows)
SELECT ctid, confrelid
FROM pg_catalog.pg_constraint fk
WHERE confrelid != 0 AND
......
This diff is collapsed.
......@@ -394,6 +394,22 @@ SET CONSTRAINTS ALL IMMEDIATE; -- should fail
COMMIT;
-- test deferrable UNIQUE with a partitioned table
CREATE TABLE parted_uniq_tbl (i int UNIQUE DEFERRABLE) partition by range (i);
CREATE TABLE parted_uniq_tbl_1 PARTITION OF parted_uniq_tbl FOR VALUES FROM (0) TO (10);
CREATE TABLE parted_uniq_tbl_2 PARTITION OF parted_uniq_tbl FOR VALUES FROM (20) TO (30);
SELECT conname, conrelid::regclass FROM pg_constraint
WHERE conname LIKE 'parted_uniq%' ORDER BY conname;
BEGIN;
INSERT INTO parted_uniq_tbl VALUES (1);
SAVEPOINT f;
INSERT INTO parted_uniq_tbl VALUES (1); -- unique violation
ROLLBACK TO f;
SET CONSTRAINTS parted_uniq_tbl_i_key DEFERRED;
INSERT INTO parted_uniq_tbl VALUES (1); -- OK now, fail at commit
COMMIT;
DROP TABLE parted_uniq_tbl;
-- test a HOT update that invalidates the conflicting tuple.
-- the trigger should still fire and catch the violation
......
......@@ -547,6 +547,32 @@ SET CONSTRAINTS ALL IMMEDIATE; -- should fail
ERROR: duplicate key value violates unique constraint "unique_tbl_i_key"
DETAIL: Key (i)=(3) already exists.
COMMIT;
-- test deferrable UNIQUE with a partitioned table
CREATE TABLE parted_uniq_tbl (i int UNIQUE DEFERRABLE) partition by range (i);
CREATE TABLE parted_uniq_tbl_1 PARTITION OF parted_uniq_tbl FOR VALUES FROM (0) TO (10);
CREATE TABLE parted_uniq_tbl_2 PARTITION OF parted_uniq_tbl FOR VALUES FROM (20) TO (30);
SELECT conname, conrelid::regclass FROM pg_constraint
WHERE conname LIKE 'parted_uniq%' ORDER BY conname;
conname | conrelid
-------------------------+-------------------
parted_uniq_tbl_1_i_key | parted_uniq_tbl_1
parted_uniq_tbl_2_i_key | parted_uniq_tbl_2
parted_uniq_tbl_i_key | parted_uniq_tbl
(3 rows)
BEGIN;
INSERT INTO parted_uniq_tbl VALUES (1);
SAVEPOINT f;
INSERT INTO parted_uniq_tbl VALUES (1); -- unique violation
ERROR: duplicate key value violates unique constraint "parted_uniq_tbl_1_i_key"
DETAIL: Key (i)=(1) already exists.
ROLLBACK TO f;
SET CONSTRAINTS parted_uniq_tbl_i_key DEFERRED;
INSERT INTO parted_uniq_tbl VALUES (1); -- OK now, fail at commit
COMMIT;
ERROR: duplicate key value violates unique constraint "parted_uniq_tbl_1_i_key"
DETAIL: Key (i)=(1) already exists.
DROP TABLE parted_uniq_tbl;
-- test a HOT update that invalidates the conflicting tuple.
-- the trigger should still fire and catch the violation
BEGIN;
......
......@@ -185,6 +185,10 @@ SELECT ctid, conindid
FROM pg_catalog.pg_constraint fk
WHERE conindid != 0 AND
NOT EXISTS(SELECT 1 FROM pg_catalog.pg_class pk WHERE pk.oid = fk.conindid);
SELECT ctid, conparentid
FROM pg_catalog.pg_constraint fk
WHERE conparentid != 0 AND
NOT EXISTS(SELECT 1 FROM pg_catalog.pg_constraint pk WHERE pk.oid = fk.conparentid);
SELECT ctid, confrelid
FROM pg_catalog.pg_constraint fk
WHERE confrelid != 0 AND
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment