From: Alvaro Herrera <alvherre@alvh.no-ip.org>
To: Robert Haas <robertmhaas@gmail.com>
Cc: David Rowley <david.rowley@2ndquadrant.com>
Cc: Simon Riggs <simon@2ndquadrant.com>
Cc: Pg Hackers <pgsql-hackers@postgresql.org>
Cc: Maksim Milyutin <milyutinma@gmail.com>
Subject: Re: [HACKERS] Proposal: Local indexes for partitioned table
Date: Wed, 20 Dec 2017 16:49:37 -0300
Message-ID: <20171220194937.pldcecyx7yrwmgkg@alvherre.pgsql> (raw)
In-Reply-To: <20171220192414.xccwfqooih5ktlu5@alvherre.pgsql>
Just to show what I'm talking about, here are a few prototype patches.
Beware, these are all very lightly tested/reviewed. Input is welcome,
particularly if it comes in the guise of patches to regression tests
showing cases that misbehave.
0001 is a fixup for the v6 patch I posted upthread; it's needed so the
0002 patch doesn't end up with indexes marked invalid after pg_dump.
--
Álvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
From bd26504debb14567de780da10bf859b205326b46 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Wed, 20 Dec 2017 16:31:05 -0300
Subject: [PATCH v7 1/4] Fixup! Local partitioned indexes v6
Don't actually mark an index invalid if it's on a plain table,
rather than a partition, even with ONLY specified.
---
src/backend/commands/indexcmds.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.cindex a757f05dd5..e925351056 100644--- a/src/backend/commands/indexcmds.c+++ b/src/backend/commands/indexcmds.c@@ -723,7 +723,7 @@ DefineIndex(Oid relationId,
flags |= INDEX_CREATE_PARTITIONED;
if (stmt->primary)
flags |= INDEX_CREATE_IS_PRIMARY;
- if (stmt->relation && !stmt->relation->inh)+ if (partitioned && stmt->relation && !stmt->relation->inh)
flags |= INDEX_CREATE_INVALID;
if (stmt->deferrable)
--
2.11.0
From b10f9afbe4b1c46c7a2706c70a398307732ac9fc Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Mon, 6 Nov 2017 17:04:55 +0100
Subject: [PATCH v7 2/4] allow indexes on partitioned tables to be unique
---
src/backend/commands/indexcmds.c | 71 +++++++++++++--
src/backend/parser/parse_utilcmd.c | 24 -----
src/test/regress/expected/alter_table.out | 8 --
src/test/regress/expected/create_table.out | 12 ---
src/test/regress/expected/indexing.out | 142 ++++++++++++++++++++++++++++-
src/test/regress/sql/alter_table.sql | 2 -
src/test/regress/sql/create_table.sql | 8 --
src/test/regress/sql/indexing.sql | 73 ++++++++++++++-
8 files changed, 274 insertions(+), 66 deletions(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.cindex e925351056..86dcab6136 100644--- a/src/backend/commands/indexcmds.c+++ b/src/backend/commands/indexcmds.c@@ -426,20 +426,11 @@ DefineIndex(Oid relationId,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot create index on partitioned table \"%s\" concurrently",
RelationGetRelationName(rel))));
- if (stmt->unique)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("cannot create unique index on partitioned table \"%s\"",- RelationGetRelationName(rel))));
if (stmt->excludeOpNames)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot create exclusion constraints on partitioned table \"%s\"",
RelationGetRelationName(rel))));
- if (stmt->primary || stmt->isconstraint)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("cannot create constraints on partitioned tables")));
}
/*
@@ -637,6 +628,68 @@ DefineIndex(Oid relationId,
index_check_primary_key(rel, indexInfo, is_alter_table);
/*
+ * If this table is partitioned and we're creating a unique index or a+ * primary key, make sure that the indexed columns are part of the+ * partition key. Otherwise it would be possible to violate uniqueness by+ * putting values that ought to be unique in different partitions.+ *+ * We could lift this limitation if we had global indexes, but those have+ * their own problems, so this is a useful feature combination.+ */+ if (partitioned && (stmt->unique || stmt->primary))+ {+ PartitionKey key = rel->rd_partkey;+ int i;++ /*+ * A partitioned table can have unique indexes, as long as all the+ * columns in the partition key appear in the unique key. A+ * partition-local index can enforce global uniqueness iff the PK+ * value completely determines the partition that a row is in.+ *+ * Thus, verify that all the columns in the partition key appear+ * in the unique key definition.+ */+ for (i = 0; i < key->partnatts; i++)+ {+ bool found = false;+ int j;++ /*+ * It may be possible to support UNIQUE constraints when partition+ * keys are expressions, but is it worth it? Give up for now.+ */+ if (key->partattrs[i] == 0)+ ereport(ERROR,+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),+ /* XXX reformulate error message? */+ errmsg("UNIQUE constraints are not supported on partitioned tables using expressions as partition keys")));++ for (j = 0; j < indexInfo->ii_NumIndexAttrs; j++)+ {+ if (key->partattrs[i] == indexInfo->ii_KeyAttrNumbers[j])+ {+ found = true;+ break;+ }+ }+ if (!found)+ {+ Form_pg_attribute att;++ att = TupleDescAttr(RelationGetDescr(rel), key->partattrs[i] - 1);+ ereport(ERROR,+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),+ errmsg("insufficient columns in UNIQUE constraint definition"),+ errdetail("UNIQUE constraint on table \"%s\" does not include column \"%s\" which is part of the partition key.",+ RelationGetRelationName(rel),+ NameStr(att->attname))));+ }+ }+ }+++ /*
* We disallow indexes on system columns other than OID. They would not
* necessarily get updated correctly, and they don't seem useful anyway.
*/
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.cindex 45f6ec2820..e80edd3668 100644--- a/src/backend/parser/parse_utilcmd.c+++ b/src/backend/parser/parse_utilcmd.c@@ -704,12 +704,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
errmsg("primary key constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("primary key constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
/* FALL THRU */
case CONSTR_UNIQUE:
@@ -719,12 +713,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
errmsg("unique constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("unique constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
if (constraint->keys == NIL)
constraint->keys = list_make1(makeString(column->colname));
cxt->ixconstraints = lappend(cxt->ixconstraints, constraint);
@@ -821,12 +809,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
errmsg("primary key constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("primary key constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
cxt->ixconstraints = lappend(cxt->ixconstraints, constraint);
break;
@@ -837,12 +819,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
errmsg("unique constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("unique constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
cxt->ixconstraints = lappend(cxt->ixconstraints, constraint);
break;
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.outindex 517fb080bd..2caf930242 100644--- a/src/test/regress/expected/alter_table.out+++ b/src/test/regress/expected/alter_table.out@@ -3290,14 +3290,6 @@ CREATE TABLE partitioned (
a int,
b int
) PARTITION BY RANGE (a, (a+b+1));
-ALTER TABLE partitioned ADD UNIQUE (a);-ERROR: unique constraints are not supported on partitioned tables-LINE 1: ALTER TABLE partitioned ADD UNIQUE (a);- ^-ALTER TABLE partitioned ADD PRIMARY KEY (a);-ERROR: primary key constraints are not supported on partitioned tables-LINE 1: ALTER TABLE partitioned ADD PRIMARY KEY (a);- ^
ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;
ERROR: foreign key constraints are not supported on partitioned tables
LINE 1: ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.outindex 8e745402ae..866cc99b9f 100644--- a/src/test/regress/expected/create_table.out+++ b/src/test/regress/expected/create_table.out@@ -276,12 +276,6 @@ CREATE TABLE partitioned (
) PARTITION BY LIST (a1, a2); -- fail
ERROR: cannot use "list" partition strategy with more than one column
-- unsupported constraint type for partitioned tables
-CREATE TABLE partitioned (- a int PRIMARY KEY-) PARTITION BY RANGE (a);-ERROR: primary key constraints are not supported on partitioned tables-LINE 2: a int PRIMARY KEY- ^
CREATE TABLE pkrel (
a int PRIMARY KEY
);
@@ -293,12 +287,6 @@ LINE 2: a int REFERENCES pkrel(a)
^
DROP TABLE pkrel;
CREATE TABLE partitioned (
- a int UNIQUE-) PARTITION BY RANGE (a);-ERROR: unique constraints are not supported on partitioned tables-LINE 2: a int UNIQUE- ^-CREATE TABLE partitioned (
a int,
EXCLUDE USING gist (a WITH &&)
) PARTITION BY RANGE (a);
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.outindex 2904ad4cf4..812e570abc 100644--- a/src/test/regress/expected/indexing.out+++ b/src/test/regress/expected/indexing.out@@ -25,8 +25,6 @@ drop table idxpart;
-- Some unsupported features
create table idxpart (a int, b int, c text) partition by range (a);
create table idxpart1 partition of idxpart for values from (0) to (10);
-create unique index on idxpart (a);-ERROR: cannot create unique index on partitioned table "idxpart"
create index concurrently on idxpart (a);
ERROR: cannot create index on partitioned table "idxpart" concurrently
drop table idxpart;
@@ -404,3 +402,143 @@ select attrelid::regclass, attname, attnum from pg_attribute
(7 rows)
drop table idxpart;
+--+-- Constraint-related indexes+--+-- Verify that it works to add primary key / unique to partitioned tables+create table idxpart (a int primary key, b int) partition by range (a);+\d idxpart+ Table "public.idxpart"+ Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+---------+ a | integer | | not null | + b | integer | | | +Partition key: RANGE (a)+Indexes:+ "idxpart_pkey" PRIMARY KEY, btree (a)+Number of partitions: 0++drop table idxpart;+-- but not if you fail to use the full partition key+create table idxpart (a int unique, b int) partition by range (a, b);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "b" which is part of the partition key.+create table idxpart (a int, b int unique) partition by range (a, b);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "a" which is part of the partition key.+create table idxpart (a int primary key, b int) partition by range (b, a);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "b" which is part of the partition key.+create table idxpart (a int, b int primary key) partition by range (b, a);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "a" which is part of the partition key.+-- OK if you use them in some other order+create table idxpart (a int, b int, c text, primary key (a, b, c)) partition by range (b, c, a);+drop table idxpart;+create table idxpart (a int primary key, b int) partition by range ((b + a));+ERROR: UNIQUE constraints are not supported on partitioned tables using expressions as partition keys+-- not other types of index-based constraints+create table idxpart (a int, exclude (a with = )) partition by range (a);+ERROR: exclusion constraints are not supported on partitioned tables+LINE 1: create table idxpart (a int, exclude (a with = )) partition ...+ ^+-- It works to add primary keys after the partitioned table is created+create table idxpart (a int, b int, c text) partition by range (a, b);+alter table idxpart add primary key (a); -- not an incomplete one tho+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "b" which is part of the partition key.+alter table idxpart add primary key (a, b);+\d idxpart+ Table "public.idxpart"+ Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+---------+ a | integer | | not null | + b | integer | | not null | + c | text | | | +Partition key: RANGE (a, b)+Indexes:+ "idxpart_pkey" PRIMARY KEY, btree (a, b)+Number of partitions: 0++create table idxpart1 partition of idxpart for values from (0, 0) to (1000, 1000);+\d idxpart1+ Table "public.idxpart1"+ Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+---------+ a | integer | | not null | + b | integer | | not null | + c | text | | | +Partition of: idxpart FOR VALUES FROM (0, 0) TO (1000, 1000)+Indexes:+ "idxpart1_pkey" PRIMARY KEY, btree (a, b)++drop table idxpart;+-- It works to add unique constraints after the partitioned table is created+create table idxpart (a int, b int) partition by range (a, b);+alter table idxpart add unique (a); -- ... nope+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "b" which is part of the partition key.+alter table idxpart add unique (b, a);+\d idxpart+ Table "public.idxpart"+ Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+---------+ a | integer | | | + b | integer | | | +Partition key: RANGE (a, b)+Indexes:+ "idxpart_b_a_key" UNIQUE CONSTRAINT, btree (b, a)+Number of partitions: 0++drop table idxpart;+-- Exclusion constraints cannot be added+create table idxpart (a int, b int) partition by range (a);+alter table idxpart add exclude (a with =);+ERROR: exclusion constraints are not supported on partitioned tables+LINE 1: alter table idxpart add exclude (a with =);+ ^+drop table idxpart;+-- When (sub)partitions are created, they also contain the constraint+create table idxpart (a int, b int, primary key (a, b)) partition by range (a, b);+create table idxpart1 partition of idxpart for values from (1, 1) to (10, 10);+create table idxpart2 partition of idxpart for values from (10, 10) to (20, 20)+ partition by range (b);+create table idxpart21 partition of idxpart2 for values from (10) to (15);+create table idxpart22 partition of idxpart2 for values from (15) to (20);+create table idxpart3 (b int not null, a int not null);+alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 30);+select conname, contype, conrelid::regclass, conindid::regclass, conkey+ from pg_constraint where conrelid::regclass::text like 'idxpart%'+ order by conname;+ conname | contype | conrelid | conindid | conkey +----------------+---------+-----------+----------------+--------+ idxpart1_pkey | p | idxpart1 | idxpart1_pkey | {1,2}+ idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2}+ idxpart22_pkey | p | idxpart22 | idxpart22_pkey | {1,2}+ idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2}+ idxpart3_pkey | p | idxpart3 | idxpart3_pkey | {2,1}+ idxpart_pkey | p | idxpart | idxpart_pkey | {1,2}+(6 rows)++drop table idxpart;+-- multi-layer partitioning honors the prohibition. So this fails:+create table idxpart (a int, b int, primary key (a)) partition by range (a);+create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart2" does not include column "b" which is part of the partition key.+drop table idxpart;+-- but this works:+create table idxpart (a int, b int, primary key (a, b)) partition by range (a);+create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b);+create table idxpart21 partition of idxpart2 for values from (0) to (1000);+select conname, contype, conrelid::regclass, conindid::regclass, conkey+ from pg_constraint where conrelid::regclass::text like 'idxpart%'+ order by conname;+ conname | contype | conrelid | conindid | conkey +----------------+---------+-----------+----------------+--------+ idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2}+ idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2}+ idxpart_pkey | p | idxpart | idxpart_pkey | {1,2}+(3 rows)++drop table idxpart;diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sqlindex af25ee9e77..ed0bb7845b 100644--- a/src/test/regress/sql/alter_table.sql+++ b/src/test/regress/sql/alter_table.sql@@ -2016,8 +2016,6 @@ CREATE TABLE partitioned (
a int,
b int
) PARTITION BY RANGE (a, (a+b+1));
-ALTER TABLE partitioned ADD UNIQUE (a);-ALTER TABLE partitioned ADD PRIMARY KEY (a);
ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;
ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sqlindex 8f9991ef18..fefccf21a2 100644--- a/src/test/regress/sql/create_table.sql+++ b/src/test/regress/sql/create_table.sql@@ -294,10 +294,6 @@ CREATE TABLE partitioned (
) PARTITION BY LIST (a1, a2); -- fail
-- unsupported constraint type for partitioned tables
-CREATE TABLE partitioned (- a int PRIMARY KEY-) PARTITION BY RANGE (a);-
CREATE TABLE pkrel (
a int PRIMARY KEY
);
@@ -307,10 +303,6 @@ CREATE TABLE partitioned (
DROP TABLE pkrel;
CREATE TABLE partitioned (
- a int UNIQUE-) PARTITION BY RANGE (a);--CREATE TABLE partitioned (
a int,
EXCLUDE USING gist (a WITH &&)
) PARTITION BY RANGE (a);
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sqlindex 06b240ff54..bb23cd8777 100644--- a/src/test/regress/sql/indexing.sql+++ b/src/test/regress/sql/indexing.sql@@ -14,7 +14,6 @@ drop table idxpart;
-- Some unsupported features
create table idxpart (a int, b int, c text) partition by range (a);
create table idxpart1 partition of idxpart for values from (0) to (10);
-create unique index on idxpart (a);
create index concurrently on idxpart (a);
drop table idxpart;
@@ -171,3 +170,75 @@ select attrelid::regclass, attname, attnum from pg_attribute
where attrelid::regclass::text like 'idxpart%' and attnum > 0
order by attrelid::regclass, attnum;
drop table idxpart;
++--+-- Constraint-related indexes+--++-- Verify that it works to add primary key / unique to partitioned tables+create table idxpart (a int primary key, b int) partition by range (a);+\d idxpart+drop table idxpart;++-- but not if you fail to use the full partition key+create table idxpart (a int unique, b int) partition by range (a, b);+create table idxpart (a int, b int unique) partition by range (a, b);+create table idxpart (a int primary key, b int) partition by range (b, a);+create table idxpart (a int, b int primary key) partition by range (b, a);++-- OK if you use them in some other order+create table idxpart (a int, b int, c text, primary key (a, b, c)) partition by range (b, c, a);+drop table idxpart;++create table idxpart (a int primary key, b int) partition by range ((b + a));+-- not other types of index-based constraints+create table idxpart (a int, exclude (a with = )) partition by range (a);++-- It works to add primary keys after the partitioned table is created+create table idxpart (a int, b int, c text) partition by range (a, b);+alter table idxpart add primary key (a); -- not an incomplete one tho+alter table idxpart add primary key (a, b);+\d idxpart+create table idxpart1 partition of idxpart for values from (0, 0) to (1000, 1000);+\d idxpart1+drop table idxpart;++-- It works to add unique constraints after the partitioned table is created+create table idxpart (a int, b int) partition by range (a, b);+alter table idxpart add unique (a); -- ... nope+alter table idxpart add unique (b, a);+\d idxpart+drop table idxpart;++-- Exclusion constraints cannot be added+create table idxpart (a int, b int) partition by range (a);+alter table idxpart add exclude (a with =);+drop table idxpart;++-- When (sub)partitions are created, they also contain the constraint+create table idxpart (a int, b int, primary key (a, b)) partition by range (a, b);+create table idxpart1 partition of idxpart for values from (1, 1) to (10, 10);+create table idxpart2 partition of idxpart for values from (10, 10) to (20, 20)+ partition by range (b);+create table idxpart21 partition of idxpart2 for values from (10) to (15);+create table idxpart22 partition of idxpart2 for values from (15) to (20);+create table idxpart3 (b int not null, a int not null);+alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 30);+select conname, contype, conrelid::regclass, conindid::regclass, conkey+ from pg_constraint where conrelid::regclass::text like 'idxpart%'+ order by conname;+drop table idxpart;++-- multi-layer partitioning honors the prohibition. So this fails:+create table idxpart (a int, b int, primary key (a)) partition by range (a);+create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b);+drop table idxpart;++-- but this works:+create table idxpart (a int, b int, primary key (a, b)) partition by range (a);+create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b);+create table idxpart21 partition of idxpart2 for values from (0) to (1000);+select conname, contype, conrelid::regclass, conindid::regclass, conkey+ from pg_constraint where conrelid::regclass::text like 'idxpart%'+ order by conname;+drop table idxpart;
--
2.11.0
From 52e0de2aa3ac6941c429a237ad3123902cda5f10 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 21 Nov 2017 15:53:11 -0300
Subject: [PATCH v7 3/4] Allow FOR EACH ROW triggers on partitioned tables
---
src/backend/commands/trigger.c | 73 +++++++++++++++++++++++++++++++---
src/test/regress/expected/triggers.out | 55 ++++++++++++++++++++-----
src/test/regress/sql/triggers.sql | 42 ++++++++++++++-----
3 files changed, 142 insertions(+), 28 deletions(-)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.cindex 92ae3822d8..15a3cd0bff 100644--- a/src/backend/commands/trigger.c+++ b/src/backend/commands/trigger.c@@ -133,6 +133,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* relation, as well as ACL_EXECUTE on the trigger function. For internal
* triggers the caller must apply any required permission checks.
*
+ * When called on partitioned tables, this function recurses to create the+ * trigger on all the partitions, except if isInternal is true, in which+ * case caller is expected to execute recursion on its own.+ *
* Note: can return InvalidObjectAddress if we decided to not create a trigger
* at all, but a foreign-key constraint. This is a kluge for backwards
* compatibility.
@@ -179,8 +183,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
* Triggers must be on tables or views, and there are additional
* relation-type-specific restrictions.
*/
- if (rel->rd_rel->relkind == RELKIND_RELATION ||- rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)+ if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/* Tables can't have INSTEAD OF triggers */
if (stmt->timing != TRIGGER_TYPE_BEFORE &&
@@ -190,13 +193,46 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
errmsg("\"%s\" is a table",
RelationGetRelationName(rel)),
errdetail("Tables cannot have INSTEAD OF triggers.")));
- /* Disallow ROW triggers on partitioned tables */- if (stmt->row && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)+ }+ else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)+ {+ /* Partitioned tables can't have INSTEAD OF triggers */+ if (stmt->timing != TRIGGER_TYPE_BEFORE &&+ stmt->timing != TRIGGER_TYPE_AFTER)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is a partitioned table",+ errmsg("\"%s\" is a table",
RelationGetRelationName(rel)),
- errdetail("Partitioned tables cannot have ROW triggers.")));+ errdetail("Tables cannot have INSTEAD OF triggers.")));++ /*+ * FOR EACH ROW triggers have further restrictions+ */+ if (stmt->row)+ {+ /*+ * Disallow WHEN clauses; I think it's okay, but disallow for now+ * to reduce testing surface.+ */+ if (stmt->whenClause)+ ereport(ERROR,+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),+ errmsg("\"%s\" is a partitioned table",+ RelationGetRelationName(rel)),+ errdetail("Triggers FOR EACH ROW on partitioned table cannot have WHEN clauses.")));++ /*+ * BEFORE triggers FOR EACH ROW are forbidden, because they would+ * allow the user to direct the row to another partition, which+ * isn't implemented in the executor.+ */+ if (stmt->timing != TRIGGER_TYPE_AFTER)+ ereport(ERROR,+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),+ errmsg("\"%s\" is a partitioned table",+ RelationGetRelationName(rel)),+ errdetail("Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.")));+ }
}
else if (rel->rd_rel->relkind == RELKIND_VIEW)
{
@@ -982,6 +1018,31 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
InvokeObjectPostCreateHookArg(TriggerRelationId, trigoid, 0,
isInternal);
+ /*+ * If this is a FOR EACH ROW trigger on a partitioned table, recurse for+ * each partition if invoked directly by user (otherwise, caller must do+ * its own recursion).+ */+ if (stmt->row && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&+ !isInternal)+ {+ PartitionDesc partdesc = RelationGetPartitionDesc(rel);+ int i;++ for (i = 0; i < partdesc->nparts; i++)+ {+ /* XXX must create a separate constraint for each child */+ Assert(constraintOid == InvalidOid);+ /* XXX must create a separate index for each child */+ Assert(indexOid == InvalidOid);++ CreateTrigger(copyObject(stmt), queryString,+ partdesc->oids[i], refRelOid,+ constraintOid, indexOid,+ isInternal);+ }+ }+
/* Keep lock on target rel until end of xact */
heap_close(rel, NoLock);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.outindex 85d948741e..e3a88aceca 100644--- a/src/test/regress/expected/triggers.out+++ b/src/test/regress/expected/triggers.out@@ -1824,7 +1824,7 @@ create or replace function trigger_notice() returns trigger as $$
return null;
end;
$$ language plpgsql;
--- insert/update/delete statment-level triggers on the parent+-- insert/update/delete statement-level triggers on the parent
create trigger trig_ins_before before insert on parted_stmt_trig
for each statement execute procedure trigger_notice();
create trigger trig_ins_after after insert on parted_stmt_trig
@@ -1837,27 +1837,51 @@ create trigger trig_del_before before delete on parted_stmt_trig
for each statement execute procedure trigger_notice();
create trigger trig_del_after after delete on parted_stmt_trig
for each statement execute procedure trigger_notice();
+-- these cases are disallowed+create trigger trig_ins_before_1 before insert on parted_stmt_trig+ for each row execute procedure trigger_notice();+ERROR: "parted_stmt_trig" is a partitioned table+DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.+create trigger trig_upd_before_1 before update on parted_stmt_trig+ for each row execute procedure trigger_notice();+ERROR: "parted_stmt_trig" is a partitioned table+DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.+create trigger trig_del_before_1 before delete on parted_stmt_trig+ for each row execute procedure trigger_notice();+ERROR: "parted_stmt_trig" is a partitioned table+DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.+-- insert/update/delete row-level triggers on the parent+create trigger trig_ins_after_parent after insert on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_upd_after_parent after update on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_del_after_parent after delete on parted_stmt_trig+ for each row execute procedure trigger_notice();
-- insert/update/delete row-level triggers on the first partition
-create trigger trig_ins_before before insert on parted_stmt_trig1+create trigger trig_ins_before_child before insert on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_ins_after after insert on parted_stmt_trig1+create trigger trig_ins_after_child after insert on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_upd_before before update on parted_stmt_trig1+create trigger trig_upd_before_child before update on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_upd_after after update on parted_stmt_trig1+create trigger trig_upd_after_child after update on parted_stmt_trig1+ for each row execute procedure trigger_notice();+create trigger trig_del_before_child before delete on parted_stmt_trig1+ for each row execute procedure trigger_notice();+create trigger trig_del_after_child after delete on parted_stmt_trig1
for each row execute procedure trigger_notice();
-- insert/update/delete statement-level triggers on the parent
-create trigger trig_ins_before before insert on parted2_stmt_trig+create trigger trig_ins_before_3 before insert on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_ins_after after insert on parted2_stmt_trig+create trigger trig_ins_after_3 after insert on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_upd_before before update on parted2_stmt_trig+create trigger trig_upd_before_3 before update on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_upd_after after update on parted2_stmt_trig+create trigger trig_upd_after_3 after update on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_del_before before delete on parted2_stmt_trig+create trigger trig_del_before_3 before delete on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_del_after after delete on parted2_stmt_trig+create trigger trig_del_after_3 after delete on parted2_stmt_trig
for each statement execute procedure trigger_notice();
with ins (a) as (
insert into parted2_stmt_trig values (1), (2) returning a
@@ -1866,6 +1890,8 @@ NOTICE: trigger on parted_stmt_trig BEFORE INSERT for STATEMENT
NOTICE: trigger on parted2_stmt_trig BEFORE INSERT for STATEMENT
NOTICE: trigger on parted_stmt_trig1 BEFORE INSERT for ROW
NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
+NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW+NOTICE: trigger on parted_stmt_trig2 AFTER INSERT for ROW
NOTICE: trigger on parted2_stmt_trig AFTER INSERT for STATEMENT
NOTICE: trigger on parted_stmt_trig AFTER INSERT for STATEMENT
tableoid | a
@@ -1881,21 +1907,28 @@ NOTICE: trigger on parted_stmt_trig BEFORE UPDATE for STATEMENT
NOTICE: trigger on parted_stmt_trig1 BEFORE UPDATE for ROW
NOTICE: trigger on parted2_stmt_trig BEFORE UPDATE for STATEMENT
NOTICE: trigger on parted_stmt_trig1 AFTER UPDATE for ROW
+NOTICE: trigger on parted_stmt_trig1 AFTER UPDATE for ROW+NOTICE: trigger on parted_stmt_trig2 AFTER UPDATE for ROW
NOTICE: trigger on parted_stmt_trig AFTER UPDATE for STATEMENT
NOTICE: trigger on parted2_stmt_trig AFTER UPDATE for STATEMENT
delete from parted_stmt_trig;
NOTICE: trigger on parted_stmt_trig BEFORE DELETE for STATEMENT
+NOTICE: trigger on parted_stmt_trig1 BEFORE DELETE for ROW+NOTICE: trigger on parted_stmt_trig2 AFTER DELETE for ROW
NOTICE: trigger on parted_stmt_trig AFTER DELETE for STATEMENT
-- insert via copy on the parent
copy parted_stmt_trig(a) from stdin;
NOTICE: trigger on parted_stmt_trig BEFORE INSERT for STATEMENT
NOTICE: trigger on parted_stmt_trig1 BEFORE INSERT for ROW
NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
+NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW+NOTICE: trigger on parted_stmt_trig2 AFTER INSERT for ROW
NOTICE: trigger on parted_stmt_trig AFTER INSERT for STATEMENT
-- insert via copy on the first partition
copy parted_stmt_trig1(a) from stdin;
NOTICE: trigger on parted_stmt_trig1 BEFORE INSERT for ROW
NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
+NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
drop table parted_stmt_trig, parted2_stmt_trig;
--
-- Test the interaction between transition tables and both kinds of
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sqlindex 2b2236ed7d..7b1f17cf9a 100644--- a/src/test/regress/sql/triggers.sql+++ b/src/test/regress/sql/triggers.sql@@ -1307,7 +1307,7 @@ create or replace function trigger_notice() returns trigger as $$
end;
$$ language plpgsql;
--- insert/update/delete statment-level triggers on the parent+-- insert/update/delete statement-level triggers on the parent
create trigger trig_ins_before before insert on parted_stmt_trig
for each statement execute procedure trigger_notice();
create trigger trig_ins_after after insert on parted_stmt_trig
@@ -1321,28 +1321,48 @@ create trigger trig_del_before before delete on parted_stmt_trig
create trigger trig_del_after after delete on parted_stmt_trig
for each statement execute procedure trigger_notice();
+-- these cases are disallowed+create trigger trig_ins_before_1 before insert on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_upd_before_1 before update on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_del_before_1 before delete on parted_stmt_trig+ for each row execute procedure trigger_notice();++-- insert/update/delete row-level triggers on the parent+create trigger trig_ins_after_parent after insert on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_upd_after_parent after update on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_del_after_parent after delete on parted_stmt_trig+ for each row execute procedure trigger_notice();+
-- insert/update/delete row-level triggers on the first partition
-create trigger trig_ins_before before insert on parted_stmt_trig1+create trigger trig_ins_before_child before insert on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_ins_after after insert on parted_stmt_trig1+create trigger trig_ins_after_child after insert on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_upd_before before update on parted_stmt_trig1+create trigger trig_upd_before_child before update on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_upd_after after update on parted_stmt_trig1+create trigger trig_upd_after_child after update on parted_stmt_trig1+ for each row execute procedure trigger_notice();+create trigger trig_del_before_child before delete on parted_stmt_trig1+ for each row execute procedure trigger_notice();+create trigger trig_del_after_child after delete on parted_stmt_trig1
for each row execute procedure trigger_notice();
-- insert/update/delete statement-level triggers on the parent
-create trigger trig_ins_before before insert on parted2_stmt_trig+create trigger trig_ins_before_3 before insert on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_ins_after after insert on parted2_stmt_trig+create trigger trig_ins_after_3 after insert on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_upd_before before update on parted2_stmt_trig+create trigger trig_upd_before_3 before update on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_upd_after after update on parted2_stmt_trig+create trigger trig_upd_after_3 after update on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_del_before before delete on parted2_stmt_trig+create trigger trig_del_before_3 before delete on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_del_after after delete on parted2_stmt_trig+create trigger trig_del_after_3 after delete on parted2_stmt_trig
for each statement execute procedure trigger_notice();
with ins (a) as (
--
2.11.0
From b52a32a3b67df4b7c1f7824c923ee4b4f3a90278 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 21 Nov 2017 15:54:14 -0300
Subject: [PATCH v7 4/4] [WIP] Allow foreign key triggers on partitioned tables
---
src/backend/catalog/pg_constraint.c | 192 +++++++++++++++++++++++++++++
src/backend/commands/tablecmds.c | 105 +++++++++++++---
src/backend/parser/parse_utilcmd.c | 12 --
src/backend/utils/adt/ri_triggers.c | 50 +++-----
src/include/catalog/pg_constraint_fn.h | 2 +
src/include/commands/tablecmds.h | 4 +
src/test/regress/expected/alter_table.out | 4 -
src/test/regress/expected/create_table.out | 10 --
src/test/regress/sql/alter_table.sql | 49 +++++++-
src/test/regress/sql/create_table.sql | 8 --
src/test/regress/sql/foreign_key.sql | 30 +++++
11 files changed, 382 insertions(+), 84 deletions(-)
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.cindex 7dee6db0eb..9dc91fb67f 100644--- a/src/backend/catalog/pg_constraint.c+++ b/src/backend/catalog/pg_constraint.c@@ -26,6 +26,7 @@
#include "catalog/pg_operator.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
+#include "commands/tablecmds.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -375,6 +376,197 @@ CreateConstraintEntry(const char *constraintName,
return conOid;
}
+/*+ * For each foreign key constraint in relation parentId, create a cloned+ * copy of it for relationId.+ *+ * relationId is a partition of parentId, so we can be certain that it has+ * the same columns with the same datatypes. They may be in different order,+ * though.+ */+void+CloneForeignKeyConstraints(Oid parentId, Oid relationId)+{+ Relation pg_constraint;+ Relation rel;+ ScanKeyData key;+ SysScanDesc scan;+ TupleDesc tupdesc;+ HeapTuple tuple;++ /* see ATAddForeignKeyConstraint about lock level */+ rel = heap_open(relationId, AccessExclusiveLock);++ pg_constraint = heap_open(ConstraintRelationId, RowShareLock);+ tupdesc = RelationGetDescr(pg_constraint);++ ScanKeyInit(&key,+ Anum_pg_constraint_conrelid, BTEqualStrategyNumber,+ F_OIDEQ, ObjectIdGetDatum(parentId));+ scan = systable_beginscan(pg_constraint, ConstraintRelidIndexId, true,+ NULL, 1, &key);++ while ((tuple = systable_getnext(scan)) != NULL)+ {+ Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);+ AttrNumber conkey[INDEX_MAX_KEYS];+ AttrNumber confkey[INDEX_MAX_KEYS];+ Oid conpfeqop[INDEX_MAX_KEYS];+ Oid conppeqop[INDEX_MAX_KEYS];+ Oid conffeqop[INDEX_MAX_KEYS];+ Constraint *fkconstraint;+ Oid constrOid;+ ObjectAddress parentAddr,+ childAddr;+ int nelem;+ ArrayType *arr;+ Datum datum;+ bool isnull;++ /* only foreign keys */+ if (constrForm->contype != CONSTRAINT_FOREIGN)+ continue;++ ObjectAddressSet(parentAddr, ConstraintRelationId,+ HeapTupleGetOid(tuple));++ datum = fastgetattr(tuple, Anum_pg_constraint_conkey,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conkey");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != INT2OID)+ elog(ERROR, "conkey is not a 1-D smallint array");+ memcpy(conkey, ARR_DATA_PTR(arr), nelem * sizeof(AttrNumber));++ datum = fastgetattr(tuple, Anum_pg_constraint_confkey,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null confkey");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != INT2OID)+ elog(ERROR, "confkey is not a 1-D smallint array");+ memcpy(confkey, ARR_DATA_PTR(arr), nelem * sizeof(AttrNumber));++ datum = fastgetattr(tuple, Anum_pg_constraint_conpfeqop,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conpfeqop");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != OIDOID)+ elog(ERROR, "conpfeqop is not a 1-D OID array");+ memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));++ datum = fastgetattr(tuple, Anum_pg_constraint_conpfeqop,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conpfeqop");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != OIDOID)+ elog(ERROR, "conpfeqop is not a 1-D OID array");+ memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));++ datum = fastgetattr(tuple, Anum_pg_constraint_conppeqop,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conppeqop");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != OIDOID)+ elog(ERROR, "conppeqop is not a 1-D OID array");+ memcpy(conppeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));++ datum = fastgetattr(tuple, Anum_pg_constraint_conffeqop,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conffeqop");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != OIDOID)+ elog(ERROR, "conffeqop is not a 1-D OID array");+ memcpy(conffeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));++ constrOid =+ CreateConstraintEntry(NameStr(constrForm->conname),+ constrForm->connamespace,+ CONSTRAINT_FOREIGN,+ constrForm->condeferrable,+ constrForm->condeferred,+ constrForm->convalidated,+ relationId,+ conkey,+ nelem,+ InvalidOid, /* not a domain constraint */+ constrForm->conindid, /* same index */+ constrForm->confrelid, /* same foreign rel */+ confkey,+ conpfeqop,+ conppeqop,+ conffeqop,+ nelem,+ constrForm->confupdtype,+ constrForm->confdeltype,+ constrForm->confmatchtype,+ NULL,+ NULL,+ NULL,+ NULL,+ false,+ 1, false, true);++ ObjectAddressSet(childAddr, ConstraintRelationId, constrOid);+ recordDependencyOn(&childAddr, &parentAddr, DEPENDENCY_INTERNAL);++ fkconstraint = makeNode(Constraint);+ /* for now this is all we need */+ fkconstraint->fk_upd_action = constrForm->confupdtype;+ fkconstraint->fk_del_action = constrForm->confdeltype;+ fkconstraint->deferrable = constrForm->condeferrable;+ fkconstraint->initdeferred = constrForm->condeferred;++ createForeignKeyTriggers(rel, constrForm->confrelid, fkconstraint,+ constrOid, constrForm->conindid);++ /*+ * XXX Normal constraint creation can be invoked during ALTER and+ * so it needs ALTER TABLE's phase 3 checking. Current caller is just+ * CREATE TABLE .. PARTITION OF so we don't need it, but maybe for+ * ALTER TABLE .. ATTACH PARTITION we'll need it.+ */+ }+ systable_endscan(scan);++ heap_close(rel, NoLock); /* keep lock till commit */+ heap_close(pg_constraint, RowShareLock);+}
/*
* Test whether given name is currently used as a constraint name
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.cindex 47cb7c13ef..e59d774a1b 100644--- a/src/backend/commands/tablecmds.c+++ b/src/backend/commands/tablecmds.c@@ -337,9 +337,6 @@ static void validateCheckConstraint(Relation rel, HeapTuple constrtup);
static void validateForeignKeyConstraint(char *conname,
Relation rel, Relation pkrel,
Oid pkindOid, Oid constraintOid);
-static void createForeignKeyTriggers(Relation rel, Oid refRelOid,- Constraint *fkconstraint,- Oid constraintOid, Oid indexOid);
static void ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode);
static void ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -410,8 +407,10 @@ static ObjectAddress ATAddCheckConstraint(List **wqueue,
Constraint *constr,
bool recurse, bool recursing, bool is_readd,
LOCKMODE lockmode);
-static ObjectAddress ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,- Constraint *fkconstraint, LOCKMODE lockmode);+static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab,+ Relation rel,+ Constraint *fkconstraint, bool recurse, bool recursing,+ LOCKMODE lockmode);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -503,6 +502,7 @@ static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
* relkind: relkind to assign to the new relation
* ownerId: if not InvalidOid, use this as the new relation's owner.
* typaddress: if not null, it's set to the pg_type entry's address.
+ * queryString: for error reporting
*
* Note that permissions checks are done against current user regardless of
* ownerId. A nonzero ownerId is used when someone is creating a relation
@@ -955,6 +955,9 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
}
list_free(idxlist);
++ CloneForeignKeyConstraints(parentId, relationId);+
heap_close(parent, NoLock);
}
@@ -7034,7 +7037,8 @@ ATExecAddConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
RelationGetNamespace(rel),
NIL);
- address = ATAddForeignKeyConstraint(tab, rel, newConstraint,+ address = ATAddForeignKeyConstraint(wqueue, tab, rel,+ newConstraint, recurse, false,
lockmode);
break;
@@ -7189,8 +7193,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* We do permissions checks here, however.
*/
static ObjectAddress
-ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,- Constraint *fkconstraint, LOCKMODE lockmode)+ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,+ Constraint *fkconstraint, bool recurse,+ bool recursing, LOCKMODE lockmode)
{
Relation pkrel;
int16 pkattnum[INDEX_MAX_KEYS];
@@ -7224,12 +7229,22 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
* numbers)
*/
if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- ereport(ERROR,- (errcode(ERRCODE_WRONG_OBJECT_TYPE),- errmsg("cannot reference partitioned table \"%s\"",- RelationGetRelationName(pkrel))));+ {+ if (!recurse)+ ereport(ERROR,+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),+ errmsg("foreign key referencing partitioned table \"%s\" must not be ONLY",+ RelationGetRelationName(pkrel))));+ /* fix recursion in ATExecValidateConstraint to enable this case */+ if (fkconstraint->skip_validation && !fkconstraint->initially_valid)+ ereport(ERROR,+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),+ errmsg("cannot add NOT VALID foreign key to relation \"%s\"",+ RelationGetRelationName(pkrel))));+ }- if (pkrel->rd_rel->relkind != RELKIND_RELATION)+ if (pkrel->rd_rel->relkind != RELKIND_RELATION &&+ pkrel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("referenced relation \"%s\" is not a table",
@@ -7589,6 +7604,45 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
}
/*
+ * If this is a partitioned table, recurse to create the constraint on the+ * partitions also.+ */+ if (recurse)+ {+ List *children;+ ListCell *child;++ /* XXX why not find_all_inheritors? */+ children = find_inheritance_children(RelationGetRelid(rel), lockmode);++ foreach(child, children)+ {+ Oid childrelid = lfirst_oid(child);+ Relation childrel;+ AlteredTableInfo *childtab;+ ObjectAddress childAddr;++ /* find_inheritance_children already got lock */+ childrel = heap_open(childrelid, NoLock);+ CheckTableNotInUse(childrel, "ALTER TABLE"); /* XXX do we need this? */++ /* Find or create work queue entry for this table */+ childtab = ATGetQueueEntry(wqueue, childrel);++ /* Recurse to child */+ childAddr =+ ATAddForeignKeyConstraint(wqueue, childtab, childrel,+ fkconstraint, recurse, true,+ lockmode);++ /* make sure they go away together, or not at all */+ recordDependencyOn(&childAddr, &address, DEPENDENCY_INTERNAL);++ heap_close(childrel, NoLock);+ }+ }++ /*
* Close pk table, but keep lock until we've committed.
*/
heap_close(pkrel, NoLock);
@@ -7850,8 +7904,8 @@ ATExecValidateConstraint(Relation rel, char *constrName, bool recurse,
heap_close(refrel, NoLock);
/*
- * Foreign keys do not inherit, so we purposely ignore the- * recursion bit here+ * We disallow creating invalid foreign keys to or from+ * partitioned tables, so ignoring the recursion bit is okay.
*/
}
else if (con->contype == CONSTRAINT_CHECK)
@@ -8502,7 +8556,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* NB: if you change any trigger properties here, see also
* ATExecAlterConstraint.
*/
-static void+void
createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
Oid constraintOid, Oid indexOid)
{
@@ -8632,6 +8686,25 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
indexOid, true);
CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
indexOid, false);
++ /*+ * If this is a partitioned table, recurse to create triggers for each+ * child. We consider that one pg_constraint entry is enough; we only+ * need the triggers to appear per-partition.+ */+ if (get_rel_relkind(refRelOid) == RELKIND_PARTITIONED_TABLE)+ {+ ListCell *cell;+ List *dchildren;++ /* XXX maybe we need a stronger lock? */+ dchildren = find_inheritance_children(refRelOid, RowShareLock);+ foreach(cell, dchildren)+ {+ createForeignKeyTriggers(rel, lfirst_oid(cell), fkconstraint,+ constraintOid, indexOid);+ }+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.cindex e80edd3668..0a8b7760d1 100644--- a/src/backend/parser/parse_utilcmd.c+++ b/src/backend/parser/parse_utilcmd.c@@ -730,12 +730,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
errmsg("foreign key constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("foreign key constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
/*
* Fill in the current attribute's name and throw it into the
@@ -849,12 +843,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
errmsg("foreign key constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("foreign key constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
cxt->fkconstraints = lappend(cxt->fkconstraints, constraint);
break;
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.cindex b1ae9e5f96..f9c0e62417 100644--- a/src/backend/utils/adt/ri_triggers.c+++ b/src/backend/utils/adt/ri_triggers.c@@ -401,7 +401,7 @@ RI_FKey_check(TriggerData *trigdata)
/* ----------
* The query string built is
- * SELECT 1 FROM ONLY <pktable> x WHERE pkatt1 = $1 [AND ...]+ * SELECT 1 FROM <pktable> x WHERE pkatt1 = $1 [AND ...]
* FOR KEY SHARE OF x
* The type id's for the $ parameters are those of the
* corresponding FK attributes.
@@ -409,7 +409,7 @@ RI_FKey_check(TriggerData *trigdata)
*/
initStringInfo(&querybuf);
quoteRelationName(pkrelname, pk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM ONLY %s x", pkrelname);+ appendStringInfo(&querybuf, "SELECT 1 FROM %s x", pkrelname);
querysep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
{
@@ -537,7 +537,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
/* ----------
* The query string built is
- * SELECT 1 FROM ONLY <pktable> x WHERE pkatt1 = $1 [AND ...]+ * SELECT 1 FROM <pktable> x WHERE pkatt1 = $1 [AND ...]
* FOR KEY SHARE OF x
* The type id's for the $ parameters are those of the
* PK attributes themselves.
@@ -545,7 +545,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
*/
initStringInfo(&querybuf);
quoteRelationName(pkrelname, pk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM ONLY %s x", pkrelname);+ appendStringInfo(&querybuf, "SELECT 1 FROM %s x", pkrelname);
querysep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
{
@@ -793,7 +793,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
/* ----------
* The query string built is
- * SELECT 1 FROM ONLY <fktable> x WHERE $1 = fkatt1 [AND ...]+ * SELECT 1 FROM <fktable> x WHERE $1 = fkatt1 [AND ...]
* FOR KEY SHARE OF x
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
@@ -801,7 +801,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
*/
initStringInfo(&querybuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM ONLY %s x",+ appendStringInfo(&querybuf, "SELECT 1 FROM %s x",
fkrelname);
querysep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
@@ -951,14 +951,14 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
/* ----------
* The query string built is
- * DELETE FROM ONLY <fktable> WHERE $1 = fkatt1 [AND ...]+ * DELETE FROM <fktable> WHERE $1 = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
* ----------
*/
initStringInfo(&querybuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "DELETE FROM ONLY %s", fkrelname);+ appendStringInfo(&querybuf, "DELETE FROM %s", fkrelname);
querysep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
{
@@ -1122,7 +1122,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
/* ----------
* The query string built is
- * UPDATE ONLY <fktable> SET fkatt1 = $1 [, ...]+ * UPDATE <fktable> SET fkatt1 = $1 [, ...]
* WHERE $n = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes. Note that we are assuming
@@ -1133,7 +1133,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
initStringInfo(&querybuf);
initStringInfo(&qualbuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "UPDATE ONLY %s SET", fkrelname);+ appendStringInfo(&querybuf, "UPDATE %s SET", fkrelname);
querysep = "";
qualsep = "WHERE";
for (i = 0, j = riinfo->nkeys; i < riinfo->nkeys; i++, j++)
@@ -1342,7 +1342,7 @@ ri_setnull(TriggerData *trigdata)
/* ----------
* The query string built is
- * UPDATE ONLY <fktable> SET fkatt1 = NULL [, ...]+ * UPDATE <fktable> SET fkatt1 = NULL [, ...]
* WHERE $1 = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
@@ -1351,7 +1351,7 @@ ri_setnull(TriggerData *trigdata)
initStringInfo(&querybuf);
initStringInfo(&qualbuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "UPDATE ONLY %s SET", fkrelname);+ appendStringInfo(&querybuf, "UPDATE %s SET", fkrelname);
querysep = "";
qualsep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
@@ -1559,7 +1559,7 @@ ri_setdefault(TriggerData *trigdata)
/* ----------
* The query string built is
- * UPDATE ONLY <fktable> SET fkatt1 = DEFAULT [, ...]+ * UPDATE <fktable> SET fkatt1 = DEFAULT [, ...]
* WHERE $1 = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
@@ -1568,7 +1568,7 @@ ri_setdefault(TriggerData *trigdata)
initStringInfo(&querybuf);
initStringInfo(&qualbuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "UPDATE ONLY %s SET", fkrelname);+ appendStringInfo(&querybuf, "UPDATE %s SET", fkrelname);
querysep = "";
qualsep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
@@ -1895,8 +1895,8 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
/*----------
* The query string built is:
- * SELECT fk.keycols FROM ONLY relname fk- * LEFT OUTER JOIN ONLY pkrelname pk+ * SELECT fk.keycols FROM relname fk+ * LEFT OUTER JOIN pkrelname pk
* ON (pk.pkkeycol1=fk.keycol1 [AND ...])
* WHERE pk.pkkeycol1 IS NULL AND
* For MATCH SIMPLE:
@@ -1922,7 +1922,7 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
quoteRelationName(pkrelname, pk_rel);
quoteRelationName(fkrelname, fk_rel);
appendStringInfo(&querybuf,
- " FROM ONLY %s fk LEFT OUTER JOIN ONLY %s pk ON",+ " FROM %s fk LEFT OUTER JOIN %s pk ON",
fkrelname, pkrelname);
strcpy(pkattname, "pk.");
@@ -2345,22 +2345,6 @@ ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk)
/* Find or create a hashtable entry for the constraint */
riinfo = ri_LoadConstraintInfo(constraintOid);
- /* Do some easy cross-checks against the trigger call data */- if (rel_is_pk)- {- if (riinfo->fk_relid != trigger->tgconstrrelid ||- riinfo->pk_relid != RelationGetRelid(trig_rel))- elog(ERROR, "wrong pg_constraint entry for trigger \"%s\" on table \"%s\"",- trigger->tgname, RelationGetRelationName(trig_rel));- }- else- {- if (riinfo->fk_relid != RelationGetRelid(trig_rel) ||- riinfo->pk_relid != trigger->tgconstrrelid)- elog(ERROR, "wrong pg_constraint entry for trigger \"%s\" on table \"%s\"",- trigger->tgname, RelationGetRelationName(trig_rel));- }-
return riinfo;
}
diff --git a/src/include/catalog/pg_constraint_fn.h b/src/include/catalog/pg_constraint_fn.hindex 37b0b4ba82..438f5402fe 100644--- a/src/include/catalog/pg_constraint_fn.h+++ b/src/include/catalog/pg_constraint_fn.h@@ -56,6 +56,8 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool conNoInherit,
bool is_internal);
+extern void CloneForeignKeyConstraints(Oid parentId, Oid relationId);+
extern void RemoveConstraintById(Oid conId);
extern void RenameConstraintById(Oid conId, const char *newname);
extern void SetValidatedConstraintById(Oid conId);
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.hindex da3ff5dbee..c68515345b 100644--- a/src/include/commands/tablecmds.h+++ b/src/include/commands/tablecmds.h@@ -74,6 +74,10 @@ extern void find_composite_type_dependencies(Oid typeOid,
extern void check_of_type(HeapTuple typetuple);
+extern void createForeignKeyTriggers(Relation rel, Oid refRelOid,+ Constraint *fkconstraint, Oid constraintOid,+ Oid indexOid);+
extern void register_on_commit_action(Oid relid, OnCommitAction action);
extern void remove_on_commit_action(Oid relid);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.outindex 2caf930242..65d2058af7 100644--- a/src/test/regress/expected/alter_table.out+++ b/src/test/regress/expected/alter_table.out@@ -3290,10 +3290,6 @@ CREATE TABLE partitioned (
a int,
b int
) PARTITION BY RANGE (a, (a+b+1));
-ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;-ERROR: foreign key constraints are not supported on partitioned tables-LINE 1: ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;- ^
ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
ERROR: exclusion constraints are not supported on partitioned tables
LINE 1: ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.outindex 866cc99b9f..7c3703b73d 100644--- a/src/test/regress/expected/create_table.out+++ b/src/test/regress/expected/create_table.out@@ -276,16 +276,6 @@ CREATE TABLE partitioned (
) PARTITION BY LIST (a1, a2); -- fail
ERROR: cannot use "list" partition strategy with more than one column
-- unsupported constraint type for partitioned tables
-CREATE TABLE pkrel (- a int PRIMARY KEY-);-CREATE TABLE partitioned (- a int REFERENCES pkrel(a)-) PARTITION BY RANGE (a);-ERROR: foreign key constraints are not supported on partitioned tables-LINE 2: a int REFERENCES pkrel(a)- ^-DROP TABLE pkrel;
CREATE TABLE partitioned (
a int,
EXCLUDE USING gist (a WITH &&)
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sqlindex ed0bb7845b..3f0265b99d 100644--- a/src/test/regress/sql/alter_table.sql+++ b/src/test/regress/sql/alter_table.sql@@ -375,6 +375,54 @@ DROP TABLE tmp3;
DROP TABLE tmp2;
+-- Ensure we can add foreign keys to and from partitioned tables+SET search_path TO at_tst;+CREATE SCHEMA at_tst;+CREATE TABLE at_regular1 (col1 INT PRIMARY KEY);+CREATE TABLE at_partitioned (col2 INT PRIMARY KEY,+ col1 INT NOT NULL) PARTITION BY RANGE (col2);+CREATE TABLE at_regular2 (col2 INT);+ALTER TABLE at_regular2 ADD FOREIGN KEY (col2) REFERENCES at_partitioned;+ALTER TABLE at_partitioned ADD FOREIGN KEY (col1) REFERENCES at_regular1;+CREATE TABLE at_partitioned_0 PARTITION OF at_partitioned+ FOR VALUES FROM (0) TO (10000);+-- these fail:+INSERT INTO at_regular2 VALUES (1000);+INSERT INTO at_partitioned VALUES (1000, 42);++-- these work:+INSERT INTO at_regular1 VALUES (1000);+INSERT INTO at_partitioned VALUES (42, 1000);+INSERT INTO at_regular2 VALUES (42);++CREATE TABLE at_partitioned_1 PARTITION OF at_partitioned+ FOR VALUES FROM (10000) TO (20000);+CREATE TABLE at_partitioned_2 (col2 INT, col1 INT);+ALTER TABLE at_partitioned ATTACH PARTITION at_partitioned_2+ FOR VALUES FROM (20000) TO (30000);+ALTER TABLE at_partitioned_2 ALTER col2 SET NOT NULL;+ALTER TABLE at_partitioned ATTACH PARTITION at_partitioned_2+ FOR VALUES FROM (20000) TO (30000);++\d at_regular2+\d at_partitioned+\d at_partitioned_0+\d at_partitioned_1+\d at_partitioned_2++INSERT INTO at_partitioned VALUES (5000, 42), (15000, 1042), (25000, 2042);+INSERT INTO at_regular2 VALUES (5000, 42), (15000, 1042), (25000, 2042);+++ALTER TABLE at_regular2 DROP CONSTRAINT at_regular2_col2_fkey;+ALTER TABLE at_partitioned_0 DROP CONSTRAINT at_partitioned_col1_fkey;+ALTER TABLE at_partitioned DROP CONSTRAINT at_partitioned_col1_fkey;++\set VERBOSITY terse+DROP SCHEMA at_tst CASCADE;+\set VERBOSITY default+RESET search_path;+
-- NOT VALID with plan invalidation -- ensure we don't use a constraint for
-- exclusion until validated
set constraint_exclusion TO 'partition';
@@ -2016,7 +2064,6 @@ CREATE TABLE partitioned (
a int,
b int
) PARTITION BY RANGE (a, (a+b+1));
-ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;
ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
-- cannot drop column that is part of the partition key
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sqlindex fefccf21a2..09a634d79d 100644--- a/src/test/regress/sql/create_table.sql+++ b/src/test/regress/sql/create_table.sql@@ -294,14 +294,6 @@ CREATE TABLE partitioned (
) PARTITION BY LIST (a1, a2); -- fail
-- unsupported constraint type for partitioned tables
-CREATE TABLE pkrel (- a int PRIMARY KEY-);-CREATE TABLE partitioned (- a int REFERENCES pkrel(a)-) PARTITION BY RANGE (a);-DROP TABLE pkrel;-
CREATE TABLE partitioned (
a int,
EXCLUDE USING gist (a WITH &&)
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sqlindex 5f19dad03c..c386e33922 100644--- a/src/test/regress/sql/foreign_key.sql+++ b/src/test/regress/sql/foreign_key.sql@@ -1055,3 +1055,33 @@ alter table fktable2 drop constraint fktable2_f1_fkey;
commit;
drop table pktable2, fktable2;
+++--+-- Foreign keys and partitioned tables+--++-- Test that it's possible to have a FK from a partitioned table to a regular+-- one+CREATE TABLE pkregular (f1 int primary key);+CREATE TABLE fkpartit (f1 int references pkregular) PARTITION BY RANGE (f1);+CREATE TABLE fkpart1 PARTITION OF fkpartit FOR VALUES FROM (0) TO (1000);+INSERT INTO fkpartit VALUES (500);+INSERT INTO fkpart1 VALUES (500);+INSERT INTO pkregular VALUES (500);+INSERT INTO fkpartit VALUES (500);+INSERT INTO fkpart1 VALUES (500);+DELETE FROM pkregular;+UPDATE pkregular SET f1 = 501;++ALTER TABLE fkpart1 DROP CONSTRAINT fkpartit_f1_fkey; -- nope+ALTER TABLE fkpartit DROP CONSTRAINT fkpartit_f1_fkey;+\d fkpartit+\d fkpart1+ALTER TABLE fkpartit ADD CONSTRAINT fkpartit_f1_fkey FOREIGN KEY (f1) REFERENCES pkregular ON DELETE CASCADE;+\d fkpartit+\d fkpart1+DELETE FROM pkregular;+SELECT * FROM fkpartit;++-- Test ALTER TABLE .. ATTACH PARTITION to clone constraints
--
2.11.0
Attachments:
[text/plain] v7-0001-Fixup-Local-partitioned-indexes-v6.patch (940B, ../20171220194937.pldcecyx7yrwmgkg@alvherre.pgsql/2-v7-0001-Fixup-Local-partitioned-indexes-v6.patch)
download | inline diff:
From bd26504debb14567de780da10bf859b205326b46 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Wed, 20 Dec 2017 16:31:05 -0300
Subject: [PATCH v7 1/4] Fixup! Local partitioned indexes v6
Don't actually mark an index invalid if it's on a plain table,
rather than a partition, even with ONLY specified.
---
src/backend/commands/indexcmds.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.cindex a757f05dd5..e925351056 100644--- a/src/backend/commands/indexcmds.c+++ b/src/backend/commands/indexcmds.c@@ -723,7 +723,7 @@ DefineIndex(Oid relationId,
flags |= INDEX_CREATE_PARTITIONED;
if (stmt->primary)
flags |= INDEX_CREATE_IS_PRIMARY;
- if (stmt->relation && !stmt->relation->inh)+ if (partitioned && stmt->relation && !stmt->relation->inh)
flags |= INDEX_CREATE_INVALID;
if (stmt->deferrable)
--
2.11.0
[text/plain] v7-0002-allow-indexes-on-partitioned-tables-to-be-unique.patch (21.6K, ../20171220194937.pldcecyx7yrwmgkg@alvherre.pgsql/3-v7-0002-allow-indexes-on-partitioned-tables-to-be-unique.patch)
download | inline diff:
From b10f9afbe4b1c46c7a2706c70a398307732ac9fc Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Mon, 6 Nov 2017 17:04:55 +0100
Subject: [PATCH v7 2/4] allow indexes on partitioned tables to be unique
---
src/backend/commands/indexcmds.c | 71 +++++++++++++--
src/backend/parser/parse_utilcmd.c | 24 -----
src/test/regress/expected/alter_table.out | 8 --
src/test/regress/expected/create_table.out | 12 ---
src/test/regress/expected/indexing.out | 142 ++++++++++++++++++++++++++++-
src/test/regress/sql/alter_table.sql | 2 -
src/test/regress/sql/create_table.sql | 8 --
src/test/regress/sql/indexing.sql | 73 ++++++++++++++-
8 files changed, 274 insertions(+), 66 deletions(-)
diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.cindex e925351056..86dcab6136 100644--- a/src/backend/commands/indexcmds.c+++ b/src/backend/commands/indexcmds.c@@ -426,20 +426,11 @@ DefineIndex(Oid relationId,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot create index on partitioned table \"%s\" concurrently",
RelationGetRelationName(rel))));
- if (stmt->unique)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("cannot create unique index on partitioned table \"%s\"",- RelationGetRelationName(rel))));
if (stmt->excludeOpNames)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot create exclusion constraints on partitioned table \"%s\"",
RelationGetRelationName(rel))));
- if (stmt->primary || stmt->isconstraint)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("cannot create constraints on partitioned tables")));
}
/*
@@ -637,6 +628,68 @@ DefineIndex(Oid relationId,
index_check_primary_key(rel, indexInfo, is_alter_table);
/*
+ * If this table is partitioned and we're creating a unique index or a+ * primary key, make sure that the indexed columns are part of the+ * partition key. Otherwise it would be possible to violate uniqueness by+ * putting values that ought to be unique in different partitions.+ *+ * We could lift this limitation if we had global indexes, but those have+ * their own problems, so this is a useful feature combination.+ */+ if (partitioned && (stmt->unique || stmt->primary))+ {+ PartitionKey key = rel->rd_partkey;+ int i;++ /*+ * A partitioned table can have unique indexes, as long as all the+ * columns in the partition key appear in the unique key. A+ * partition-local index can enforce global uniqueness iff the PK+ * value completely determines the partition that a row is in.+ *+ * Thus, verify that all the columns in the partition key appear+ * in the unique key definition.+ */+ for (i = 0; i < key->partnatts; i++)+ {+ bool found = false;+ int j;++ /*+ * It may be possible to support UNIQUE constraints when partition+ * keys are expressions, but is it worth it? Give up for now.+ */+ if (key->partattrs[i] == 0)+ ereport(ERROR,+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),+ /* XXX reformulate error message? */+ errmsg("UNIQUE constraints are not supported on partitioned tables using expressions as partition keys")));++ for (j = 0; j < indexInfo->ii_NumIndexAttrs; j++)+ {+ if (key->partattrs[i] == indexInfo->ii_KeyAttrNumbers[j])+ {+ found = true;+ break;+ }+ }+ if (!found)+ {+ Form_pg_attribute att;++ att = TupleDescAttr(RelationGetDescr(rel), key->partattrs[i] - 1);+ ereport(ERROR,+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),+ errmsg("insufficient columns in UNIQUE constraint definition"),+ errdetail("UNIQUE constraint on table \"%s\" does not include column \"%s\" which is part of the partition key.",+ RelationGetRelationName(rel),+ NameStr(att->attname))));+ }+ }+ }+++ /*
* We disallow indexes on system columns other than OID. They would not
* necessarily get updated correctly, and they don't seem useful anyway.
*/
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.cindex 45f6ec2820..e80edd3668 100644--- a/src/backend/parser/parse_utilcmd.c+++ b/src/backend/parser/parse_utilcmd.c@@ -704,12 +704,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
errmsg("primary key constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("primary key constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
/* FALL THRU */
case CONSTR_UNIQUE:
@@ -719,12 +713,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
errmsg("unique constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("unique constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
if (constraint->keys == NIL)
constraint->keys = list_make1(makeString(column->colname));
cxt->ixconstraints = lappend(cxt->ixconstraints, constraint);
@@ -821,12 +809,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
errmsg("primary key constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("primary key constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
cxt->ixconstraints = lappend(cxt->ixconstraints, constraint);
break;
@@ -837,12 +819,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
errmsg("unique constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("unique constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
cxt->ixconstraints = lappend(cxt->ixconstraints, constraint);
break;
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.outindex 517fb080bd..2caf930242 100644--- a/src/test/regress/expected/alter_table.out+++ b/src/test/regress/expected/alter_table.out@@ -3290,14 +3290,6 @@ CREATE TABLE partitioned (
a int,
b int
) PARTITION BY RANGE (a, (a+b+1));
-ALTER TABLE partitioned ADD UNIQUE (a);-ERROR: unique constraints are not supported on partitioned tables-LINE 1: ALTER TABLE partitioned ADD UNIQUE (a);- ^-ALTER TABLE partitioned ADD PRIMARY KEY (a);-ERROR: primary key constraints are not supported on partitioned tables-LINE 1: ALTER TABLE partitioned ADD PRIMARY KEY (a);- ^
ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;
ERROR: foreign key constraints are not supported on partitioned tables
LINE 1: ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.outindex 8e745402ae..866cc99b9f 100644--- a/src/test/regress/expected/create_table.out+++ b/src/test/regress/expected/create_table.out@@ -276,12 +276,6 @@ CREATE TABLE partitioned (
) PARTITION BY LIST (a1, a2); -- fail
ERROR: cannot use "list" partition strategy with more than one column
-- unsupported constraint type for partitioned tables
-CREATE TABLE partitioned (- a int PRIMARY KEY-) PARTITION BY RANGE (a);-ERROR: primary key constraints are not supported on partitioned tables-LINE 2: a int PRIMARY KEY- ^
CREATE TABLE pkrel (
a int PRIMARY KEY
);
@@ -293,12 +287,6 @@ LINE 2: a int REFERENCES pkrel(a)
^
DROP TABLE pkrel;
CREATE TABLE partitioned (
- a int UNIQUE-) PARTITION BY RANGE (a);-ERROR: unique constraints are not supported on partitioned tables-LINE 2: a int UNIQUE- ^-CREATE TABLE partitioned (
a int,
EXCLUDE USING gist (a WITH &&)
) PARTITION BY RANGE (a);
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.outindex 2904ad4cf4..812e570abc 100644--- a/src/test/regress/expected/indexing.out+++ b/src/test/regress/expected/indexing.out@@ -25,8 +25,6 @@ drop table idxpart;
-- Some unsupported features
create table idxpart (a int, b int, c text) partition by range (a);
create table idxpart1 partition of idxpart for values from (0) to (10);
-create unique index on idxpart (a);-ERROR: cannot create unique index on partitioned table "idxpart"
create index concurrently on idxpart (a);
ERROR: cannot create index on partitioned table "idxpart" concurrently
drop table idxpart;
@@ -404,3 +402,143 @@ select attrelid::regclass, attname, attnum from pg_attribute
(7 rows)
drop table idxpart;
+--+-- Constraint-related indexes+--+-- Verify that it works to add primary key / unique to partitioned tables+create table idxpart (a int primary key, b int) partition by range (a);+\d idxpart+ Table "public.idxpart"+ Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+---------+ a | integer | | not null | + b | integer | | | +Partition key: RANGE (a)+Indexes:+ "idxpart_pkey" PRIMARY KEY, btree (a)+Number of partitions: 0++drop table idxpart;+-- but not if you fail to use the full partition key+create table idxpart (a int unique, b int) partition by range (a, b);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "b" which is part of the partition key.+create table idxpart (a int, b int unique) partition by range (a, b);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "a" which is part of the partition key.+create table idxpart (a int primary key, b int) partition by range (b, a);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "b" which is part of the partition key.+create table idxpart (a int, b int primary key) partition by range (b, a);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "a" which is part of the partition key.+-- OK if you use them in some other order+create table idxpart (a int, b int, c text, primary key (a, b, c)) partition by range (b, c, a);+drop table idxpart;+create table idxpart (a int primary key, b int) partition by range ((b + a));+ERROR: UNIQUE constraints are not supported on partitioned tables using expressions as partition keys+-- not other types of index-based constraints+create table idxpart (a int, exclude (a with = )) partition by range (a);+ERROR: exclusion constraints are not supported on partitioned tables+LINE 1: create table idxpart (a int, exclude (a with = )) partition ...+ ^+-- It works to add primary keys after the partitioned table is created+create table idxpart (a int, b int, c text) partition by range (a, b);+alter table idxpart add primary key (a); -- not an incomplete one tho+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "b" which is part of the partition key.+alter table idxpart add primary key (a, b);+\d idxpart+ Table "public.idxpart"+ Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+---------+ a | integer | | not null | + b | integer | | not null | + c | text | | | +Partition key: RANGE (a, b)+Indexes:+ "idxpart_pkey" PRIMARY KEY, btree (a, b)+Number of partitions: 0++create table idxpart1 partition of idxpart for values from (0, 0) to (1000, 1000);+\d idxpart1+ Table "public.idxpart1"+ Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+---------+ a | integer | | not null | + b | integer | | not null | + c | text | | | +Partition of: idxpart FOR VALUES FROM (0, 0) TO (1000, 1000)+Indexes:+ "idxpart1_pkey" PRIMARY KEY, btree (a, b)++drop table idxpart;+-- It works to add unique constraints after the partitioned table is created+create table idxpart (a int, b int) partition by range (a, b);+alter table idxpart add unique (a); -- ... nope+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart" does not include column "b" which is part of the partition key.+alter table idxpart add unique (b, a);+\d idxpart+ Table "public.idxpart"+ Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+---------+ a | integer | | | + b | integer | | | +Partition key: RANGE (a, b)+Indexes:+ "idxpart_b_a_key" UNIQUE CONSTRAINT, btree (b, a)+Number of partitions: 0++drop table idxpart;+-- Exclusion constraints cannot be added+create table idxpart (a int, b int) partition by range (a);+alter table idxpart add exclude (a with =);+ERROR: exclusion constraints are not supported on partitioned tables+LINE 1: alter table idxpart add exclude (a with =);+ ^+drop table idxpart;+-- When (sub)partitions are created, they also contain the constraint+create table idxpart (a int, b int, primary key (a, b)) partition by range (a, b);+create table idxpart1 partition of idxpart for values from (1, 1) to (10, 10);+create table idxpart2 partition of idxpart for values from (10, 10) to (20, 20)+ partition by range (b);+create table idxpart21 partition of idxpart2 for values from (10) to (15);+create table idxpart22 partition of idxpart2 for values from (15) to (20);+create table idxpart3 (b int not null, a int not null);+alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 30);+select conname, contype, conrelid::regclass, conindid::regclass, conkey+ from pg_constraint where conrelid::regclass::text like 'idxpart%'+ order by conname;+ conname | contype | conrelid | conindid | conkey +----------------+---------+-----------+----------------+--------+ idxpart1_pkey | p | idxpart1 | idxpart1_pkey | {1,2}+ idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2}+ idxpart22_pkey | p | idxpart22 | idxpart22_pkey | {1,2}+ idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2}+ idxpart3_pkey | p | idxpart3 | idxpart3_pkey | {2,1}+ idxpart_pkey | p | idxpart | idxpart_pkey | {1,2}+(6 rows)++drop table idxpart;+-- multi-layer partitioning honors the prohibition. So this fails:+create table idxpart (a int, b int, primary key (a)) partition by range (a);+create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b);+ERROR: insufficient columns in UNIQUE constraint definition+DETAIL: UNIQUE constraint on table "idxpart2" does not include column "b" which is part of the partition key.+drop table idxpart;+-- but this works:+create table idxpart (a int, b int, primary key (a, b)) partition by range (a);+create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b);+create table idxpart21 partition of idxpart2 for values from (0) to (1000);+select conname, contype, conrelid::regclass, conindid::regclass, conkey+ from pg_constraint where conrelid::regclass::text like 'idxpart%'+ order by conname;+ conname | contype | conrelid | conindid | conkey +----------------+---------+-----------+----------------+--------+ idxpart21_pkey | p | idxpart21 | idxpart21_pkey | {1,2}+ idxpart2_pkey | p | idxpart2 | idxpart2_pkey | {1,2}+ idxpart_pkey | p | idxpart | idxpart_pkey | {1,2}+(3 rows)++drop table idxpart;diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sqlindex af25ee9e77..ed0bb7845b 100644--- a/src/test/regress/sql/alter_table.sql+++ b/src/test/regress/sql/alter_table.sql@@ -2016,8 +2016,6 @@ CREATE TABLE partitioned (
a int,
b int
) PARTITION BY RANGE (a, (a+b+1));
-ALTER TABLE partitioned ADD UNIQUE (a);-ALTER TABLE partitioned ADD PRIMARY KEY (a);
ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;
ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sqlindex 8f9991ef18..fefccf21a2 100644--- a/src/test/regress/sql/create_table.sql+++ b/src/test/regress/sql/create_table.sql@@ -294,10 +294,6 @@ CREATE TABLE partitioned (
) PARTITION BY LIST (a1, a2); -- fail
-- unsupported constraint type for partitioned tables
-CREATE TABLE partitioned (- a int PRIMARY KEY-) PARTITION BY RANGE (a);-
CREATE TABLE pkrel (
a int PRIMARY KEY
);
@@ -307,10 +303,6 @@ CREATE TABLE partitioned (
DROP TABLE pkrel;
CREATE TABLE partitioned (
- a int UNIQUE-) PARTITION BY RANGE (a);--CREATE TABLE partitioned (
a int,
EXCLUDE USING gist (a WITH &&)
) PARTITION BY RANGE (a);
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sqlindex 06b240ff54..bb23cd8777 100644--- a/src/test/regress/sql/indexing.sql+++ b/src/test/regress/sql/indexing.sql@@ -14,7 +14,6 @@ drop table idxpart;
-- Some unsupported features
create table idxpart (a int, b int, c text) partition by range (a);
create table idxpart1 partition of idxpart for values from (0) to (10);
-create unique index on idxpart (a);
create index concurrently on idxpart (a);
drop table idxpart;
@@ -171,3 +170,75 @@ select attrelid::regclass, attname, attnum from pg_attribute
where attrelid::regclass::text like 'idxpart%' and attnum > 0
order by attrelid::regclass, attnum;
drop table idxpart;
++--+-- Constraint-related indexes+--++-- Verify that it works to add primary key / unique to partitioned tables+create table idxpart (a int primary key, b int) partition by range (a);+\d idxpart+drop table idxpart;++-- but not if you fail to use the full partition key+create table idxpart (a int unique, b int) partition by range (a, b);+create table idxpart (a int, b int unique) partition by range (a, b);+create table idxpart (a int primary key, b int) partition by range (b, a);+create table idxpart (a int, b int primary key) partition by range (b, a);++-- OK if you use them in some other order+create table idxpart (a int, b int, c text, primary key (a, b, c)) partition by range (b, c, a);+drop table idxpart;++create table idxpart (a int primary key, b int) partition by range ((b + a));+-- not other types of index-based constraints+create table idxpart (a int, exclude (a with = )) partition by range (a);++-- It works to add primary keys after the partitioned table is created+create table idxpart (a int, b int, c text) partition by range (a, b);+alter table idxpart add primary key (a); -- not an incomplete one tho+alter table idxpart add primary key (a, b);+\d idxpart+create table idxpart1 partition of idxpart for values from (0, 0) to (1000, 1000);+\d idxpart1+drop table idxpart;++-- It works to add unique constraints after the partitioned table is created+create table idxpart (a int, b int) partition by range (a, b);+alter table idxpart add unique (a); -- ... nope+alter table idxpart add unique (b, a);+\d idxpart+drop table idxpart;++-- Exclusion constraints cannot be added+create table idxpart (a int, b int) partition by range (a);+alter table idxpart add exclude (a with =);+drop table idxpart;++-- When (sub)partitions are created, they also contain the constraint+create table idxpart (a int, b int, primary key (a, b)) partition by range (a, b);+create table idxpart1 partition of idxpart for values from (1, 1) to (10, 10);+create table idxpart2 partition of idxpart for values from (10, 10) to (20, 20)+ partition by range (b);+create table idxpart21 partition of idxpart2 for values from (10) to (15);+create table idxpart22 partition of idxpart2 for values from (15) to (20);+create table idxpart3 (b int not null, a int not null);+alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 30);+select conname, contype, conrelid::regclass, conindid::regclass, conkey+ from pg_constraint where conrelid::regclass::text like 'idxpart%'+ order by conname;+drop table idxpart;++-- multi-layer partitioning honors the prohibition. So this fails:+create table idxpart (a int, b int, primary key (a)) partition by range (a);+create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b);+drop table idxpart;++-- but this works:+create table idxpart (a int, b int, primary key (a, b)) partition by range (a);+create table idxpart2 partition of idxpart for values from (0) to (1000) partition by range (b);+create table idxpart21 partition of idxpart2 for values from (0) to (1000);+select conname, contype, conrelid::regclass, conindid::regclass, conkey+ from pg_constraint where conrelid::regclass::text like 'idxpart%'+ order by conname;+drop table idxpart;--
2.11.0
[text/plain] v7-0003-Allow-FOR-EACH-ROW-triggers-on-partitioned-tables.patch (15.0K, ../20171220194937.pldcecyx7yrwmgkg@alvherre.pgsql/4-v7-0003-Allow-FOR-EACH-ROW-triggers-on-partitioned-tables.patch)
download | inline diff:
From 52e0de2aa3ac6941c429a237ad3123902cda5f10 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 21 Nov 2017 15:53:11 -0300
Subject: [PATCH v7 3/4] Allow FOR EACH ROW triggers on partitioned tables
---
src/backend/commands/trigger.c | 73 +++++++++++++++++++++++++++++++---
src/test/regress/expected/triggers.out | 55 ++++++++++++++++++++-----
src/test/regress/sql/triggers.sql | 42 ++++++++++++++-----
3 files changed, 142 insertions(+), 28 deletions(-)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.cindex 92ae3822d8..15a3cd0bff 100644--- a/src/backend/commands/trigger.c+++ b/src/backend/commands/trigger.c@@ -133,6 +133,10 @@ static bool before_stmt_triggers_fired(Oid relid, CmdType cmdType);
* relation, as well as ACL_EXECUTE on the trigger function. For internal
* triggers the caller must apply any required permission checks.
*
+ * When called on partitioned tables, this function recurses to create the+ * trigger on all the partitions, except if isInternal is true, in which+ * case caller is expected to execute recursion on its own.+ *
* Note: can return InvalidObjectAddress if we decided to not create a trigger
* at all, but a foreign-key constraint. This is a kluge for backwards
* compatibility.
@@ -179,8 +183,7 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
* Triggers must be on tables or views, and there are additional
* relation-type-specific restrictions.
*/
- if (rel->rd_rel->relkind == RELKIND_RELATION ||- rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)+ if (rel->rd_rel->relkind == RELKIND_RELATION)
{
/* Tables can't have INSTEAD OF triggers */
if (stmt->timing != TRIGGER_TYPE_BEFORE &&
@@ -190,13 +193,46 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
errmsg("\"%s\" is a table",
RelationGetRelationName(rel)),
errdetail("Tables cannot have INSTEAD OF triggers.")));
- /* Disallow ROW triggers on partitioned tables */- if (stmt->row && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)+ }+ else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)+ {+ /* Partitioned tables can't have INSTEAD OF triggers */+ if (stmt->timing != TRIGGER_TYPE_BEFORE &&+ stmt->timing != TRIGGER_TYPE_AFTER)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("\"%s\" is a partitioned table",+ errmsg("\"%s\" is a table",
RelationGetRelationName(rel)),
- errdetail("Partitioned tables cannot have ROW triggers.")));+ errdetail("Tables cannot have INSTEAD OF triggers.")));++ /*+ * FOR EACH ROW triggers have further restrictions+ */+ if (stmt->row)+ {+ /*+ * Disallow WHEN clauses; I think it's okay, but disallow for now+ * to reduce testing surface.+ */+ if (stmt->whenClause)+ ereport(ERROR,+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),+ errmsg("\"%s\" is a partitioned table",+ RelationGetRelationName(rel)),+ errdetail("Triggers FOR EACH ROW on partitioned table cannot have WHEN clauses.")));++ /*+ * BEFORE triggers FOR EACH ROW are forbidden, because they would+ * allow the user to direct the row to another partition, which+ * isn't implemented in the executor.+ */+ if (stmt->timing != TRIGGER_TYPE_AFTER)+ ereport(ERROR,+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),+ errmsg("\"%s\" is a partitioned table",+ RelationGetRelationName(rel)),+ errdetail("Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.")));+ }
}
else if (rel->rd_rel->relkind == RELKIND_VIEW)
{
@@ -982,6 +1018,31 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
InvokeObjectPostCreateHookArg(TriggerRelationId, trigoid, 0,
isInternal);
+ /*+ * If this is a FOR EACH ROW trigger on a partitioned table, recurse for+ * each partition if invoked directly by user (otherwise, caller must do+ * its own recursion).+ */+ if (stmt->row && rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&+ !isInternal)+ {+ PartitionDesc partdesc = RelationGetPartitionDesc(rel);+ int i;++ for (i = 0; i < partdesc->nparts; i++)+ {+ /* XXX must create a separate constraint for each child */+ Assert(constraintOid == InvalidOid);+ /* XXX must create a separate index for each child */+ Assert(indexOid == InvalidOid);++ CreateTrigger(copyObject(stmt), queryString,+ partdesc->oids[i], refRelOid,+ constraintOid, indexOid,+ isInternal);+ }+ }+
/* Keep lock on target rel until end of xact */
heap_close(rel, NoLock);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.outindex 85d948741e..e3a88aceca 100644--- a/src/test/regress/expected/triggers.out+++ b/src/test/regress/expected/triggers.out@@ -1824,7 +1824,7 @@ create or replace function trigger_notice() returns trigger as $$
return null;
end;
$$ language plpgsql;
--- insert/update/delete statment-level triggers on the parent+-- insert/update/delete statement-level triggers on the parent
create trigger trig_ins_before before insert on parted_stmt_trig
for each statement execute procedure trigger_notice();
create trigger trig_ins_after after insert on parted_stmt_trig
@@ -1837,27 +1837,51 @@ create trigger trig_del_before before delete on parted_stmt_trig
for each statement execute procedure trigger_notice();
create trigger trig_del_after after delete on parted_stmt_trig
for each statement execute procedure trigger_notice();
+-- these cases are disallowed+create trigger trig_ins_before_1 before insert on parted_stmt_trig+ for each row execute procedure trigger_notice();+ERROR: "parted_stmt_trig" is a partitioned table+DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.+create trigger trig_upd_before_1 before update on parted_stmt_trig+ for each row execute procedure trigger_notice();+ERROR: "parted_stmt_trig" is a partitioned table+DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.+create trigger trig_del_before_1 before delete on parted_stmt_trig+ for each row execute procedure trigger_notice();+ERROR: "parted_stmt_trig" is a partitioned table+DETAIL: Partitioned tables cannot have BEFORE / FOR EACH ROW triggers.+-- insert/update/delete row-level triggers on the parent+create trigger trig_ins_after_parent after insert on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_upd_after_parent after update on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_del_after_parent after delete on parted_stmt_trig+ for each row execute procedure trigger_notice();
-- insert/update/delete row-level triggers on the first partition
-create trigger trig_ins_before before insert on parted_stmt_trig1+create trigger trig_ins_before_child before insert on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_ins_after after insert on parted_stmt_trig1+create trigger trig_ins_after_child after insert on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_upd_before before update on parted_stmt_trig1+create trigger trig_upd_before_child before update on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_upd_after after update on parted_stmt_trig1+create trigger trig_upd_after_child after update on parted_stmt_trig1+ for each row execute procedure trigger_notice();+create trigger trig_del_before_child before delete on parted_stmt_trig1+ for each row execute procedure trigger_notice();+create trigger trig_del_after_child after delete on parted_stmt_trig1
for each row execute procedure trigger_notice();
-- insert/update/delete statement-level triggers on the parent
-create trigger trig_ins_before before insert on parted2_stmt_trig+create trigger trig_ins_before_3 before insert on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_ins_after after insert on parted2_stmt_trig+create trigger trig_ins_after_3 after insert on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_upd_before before update on parted2_stmt_trig+create trigger trig_upd_before_3 before update on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_upd_after after update on parted2_stmt_trig+create trigger trig_upd_after_3 after update on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_del_before before delete on parted2_stmt_trig+create trigger trig_del_before_3 before delete on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_del_after after delete on parted2_stmt_trig+create trigger trig_del_after_3 after delete on parted2_stmt_trig
for each statement execute procedure trigger_notice();
with ins (a) as (
insert into parted2_stmt_trig values (1), (2) returning a
@@ -1866,6 +1890,8 @@ NOTICE: trigger on parted_stmt_trig BEFORE INSERT for STATEMENT
NOTICE: trigger on parted2_stmt_trig BEFORE INSERT for STATEMENT
NOTICE: trigger on parted_stmt_trig1 BEFORE INSERT for ROW
NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
+NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW+NOTICE: trigger on parted_stmt_trig2 AFTER INSERT for ROW
NOTICE: trigger on parted2_stmt_trig AFTER INSERT for STATEMENT
NOTICE: trigger on parted_stmt_trig AFTER INSERT for STATEMENT
tableoid | a
@@ -1881,21 +1907,28 @@ NOTICE: trigger on parted_stmt_trig BEFORE UPDATE for STATEMENT
NOTICE: trigger on parted_stmt_trig1 BEFORE UPDATE for ROW
NOTICE: trigger on parted2_stmt_trig BEFORE UPDATE for STATEMENT
NOTICE: trigger on parted_stmt_trig1 AFTER UPDATE for ROW
+NOTICE: trigger on parted_stmt_trig1 AFTER UPDATE for ROW+NOTICE: trigger on parted_stmt_trig2 AFTER UPDATE for ROW
NOTICE: trigger on parted_stmt_trig AFTER UPDATE for STATEMENT
NOTICE: trigger on parted2_stmt_trig AFTER UPDATE for STATEMENT
delete from parted_stmt_trig;
NOTICE: trigger on parted_stmt_trig BEFORE DELETE for STATEMENT
+NOTICE: trigger on parted_stmt_trig1 BEFORE DELETE for ROW+NOTICE: trigger on parted_stmt_trig2 AFTER DELETE for ROW
NOTICE: trigger on parted_stmt_trig AFTER DELETE for STATEMENT
-- insert via copy on the parent
copy parted_stmt_trig(a) from stdin;
NOTICE: trigger on parted_stmt_trig BEFORE INSERT for STATEMENT
NOTICE: trigger on parted_stmt_trig1 BEFORE INSERT for ROW
NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
+NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW+NOTICE: trigger on parted_stmt_trig2 AFTER INSERT for ROW
NOTICE: trigger on parted_stmt_trig AFTER INSERT for STATEMENT
-- insert via copy on the first partition
copy parted_stmt_trig1(a) from stdin;
NOTICE: trigger on parted_stmt_trig1 BEFORE INSERT for ROW
NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
+NOTICE: trigger on parted_stmt_trig1 AFTER INSERT for ROW
drop table parted_stmt_trig, parted2_stmt_trig;
--
-- Test the interaction between transition tables and both kinds of
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sqlindex 2b2236ed7d..7b1f17cf9a 100644--- a/src/test/regress/sql/triggers.sql+++ b/src/test/regress/sql/triggers.sql@@ -1307,7 +1307,7 @@ create or replace function trigger_notice() returns trigger as $$
end;
$$ language plpgsql;
--- insert/update/delete statment-level triggers on the parent+-- insert/update/delete statement-level triggers on the parent
create trigger trig_ins_before before insert on parted_stmt_trig
for each statement execute procedure trigger_notice();
create trigger trig_ins_after after insert on parted_stmt_trig
@@ -1321,28 +1321,48 @@ create trigger trig_del_before before delete on parted_stmt_trig
create trigger trig_del_after after delete on parted_stmt_trig
for each statement execute procedure trigger_notice();
+-- these cases are disallowed+create trigger trig_ins_before_1 before insert on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_upd_before_1 before update on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_del_before_1 before delete on parted_stmt_trig+ for each row execute procedure trigger_notice();++-- insert/update/delete row-level triggers on the parent+create trigger trig_ins_after_parent after insert on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_upd_after_parent after update on parted_stmt_trig+ for each row execute procedure trigger_notice();+create trigger trig_del_after_parent after delete on parted_stmt_trig+ for each row execute procedure trigger_notice();+
-- insert/update/delete row-level triggers on the first partition
-create trigger trig_ins_before before insert on parted_stmt_trig1+create trigger trig_ins_before_child before insert on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_ins_after after insert on parted_stmt_trig1+create trigger trig_ins_after_child after insert on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_upd_before before update on parted_stmt_trig1+create trigger trig_upd_before_child before update on parted_stmt_trig1
for each row execute procedure trigger_notice();
-create trigger trig_upd_after after update on parted_stmt_trig1+create trigger trig_upd_after_child after update on parted_stmt_trig1+ for each row execute procedure trigger_notice();+create trigger trig_del_before_child before delete on parted_stmt_trig1+ for each row execute procedure trigger_notice();+create trigger trig_del_after_child after delete on parted_stmt_trig1
for each row execute procedure trigger_notice();
-- insert/update/delete statement-level triggers on the parent
-create trigger trig_ins_before before insert on parted2_stmt_trig+create trigger trig_ins_before_3 before insert on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_ins_after after insert on parted2_stmt_trig+create trigger trig_ins_after_3 after insert on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_upd_before before update on parted2_stmt_trig+create trigger trig_upd_before_3 before update on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_upd_after after update on parted2_stmt_trig+create trigger trig_upd_after_3 after update on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_del_before before delete on parted2_stmt_trig+create trigger trig_del_before_3 before delete on parted2_stmt_trig
for each statement execute procedure trigger_notice();
-create trigger trig_del_after after delete on parted2_stmt_trig+create trigger trig_del_after_3 after delete on parted2_stmt_trig
for each statement execute procedure trigger_notice();
with ins (a) as (
--
2.11.0
[text/plain] v7-0004-WIP-Allow-foreign-key-triggers-on-partitioned-tab.patch (29.2K, ../20171220194937.pldcecyx7yrwmgkg@alvherre.pgsql/5-v7-0004-WIP-Allow-foreign-key-triggers-on-partitioned-tab.patch)
download | inline diff:
From b52a32a3b67df4b7c1f7824c923ee4b4f3a90278 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 21 Nov 2017 15:54:14 -0300
Subject: [PATCH v7 4/4] [WIP] Allow foreign key triggers on partitioned tables
---
src/backend/catalog/pg_constraint.c | 192 +++++++++++++++++++++++++++++
src/backend/commands/tablecmds.c | 105 +++++++++++++---
src/backend/parser/parse_utilcmd.c | 12 --
src/backend/utils/adt/ri_triggers.c | 50 +++-----
src/include/catalog/pg_constraint_fn.h | 2 +
src/include/commands/tablecmds.h | 4 +
src/test/regress/expected/alter_table.out | 4 -
src/test/regress/expected/create_table.out | 10 --
src/test/regress/sql/alter_table.sql | 49 +++++++-
src/test/regress/sql/create_table.sql | 8 --
src/test/regress/sql/foreign_key.sql | 30 +++++
11 files changed, 382 insertions(+), 84 deletions(-)
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.cindex 7dee6db0eb..9dc91fb67f 100644--- a/src/backend/catalog/pg_constraint.c+++ b/src/backend/catalog/pg_constraint.c@@ -26,6 +26,7 @@
#include "catalog/pg_operator.h"
#include "catalog/pg_type.h"
#include "commands/defrem.h"
+#include "commands/tablecmds.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -375,6 +376,197 @@ CreateConstraintEntry(const char *constraintName,
return conOid;
}
+/*+ * For each foreign key constraint in relation parentId, create a cloned+ * copy of it for relationId.+ *+ * relationId is a partition of parentId, so we can be certain that it has+ * the same columns with the same datatypes. They may be in different order,+ * though.+ */+void+CloneForeignKeyConstraints(Oid parentId, Oid relationId)+{+ Relation pg_constraint;+ Relation rel;+ ScanKeyData key;+ SysScanDesc scan;+ TupleDesc tupdesc;+ HeapTuple tuple;++ /* see ATAddForeignKeyConstraint about lock level */+ rel = heap_open(relationId, AccessExclusiveLock);++ pg_constraint = heap_open(ConstraintRelationId, RowShareLock);+ tupdesc = RelationGetDescr(pg_constraint);++ ScanKeyInit(&key,+ Anum_pg_constraint_conrelid, BTEqualStrategyNumber,+ F_OIDEQ, ObjectIdGetDatum(parentId));+ scan = systable_beginscan(pg_constraint, ConstraintRelidIndexId, true,+ NULL, 1, &key);++ while ((tuple = systable_getnext(scan)) != NULL)+ {+ Form_pg_constraint constrForm = (Form_pg_constraint) GETSTRUCT(tuple);+ AttrNumber conkey[INDEX_MAX_KEYS];+ AttrNumber confkey[INDEX_MAX_KEYS];+ Oid conpfeqop[INDEX_MAX_KEYS];+ Oid conppeqop[INDEX_MAX_KEYS];+ Oid conffeqop[INDEX_MAX_KEYS];+ Constraint *fkconstraint;+ Oid constrOid;+ ObjectAddress parentAddr,+ childAddr;+ int nelem;+ ArrayType *arr;+ Datum datum;+ bool isnull;++ /* only foreign keys */+ if (constrForm->contype != CONSTRAINT_FOREIGN)+ continue;++ ObjectAddressSet(parentAddr, ConstraintRelationId,+ HeapTupleGetOid(tuple));++ datum = fastgetattr(tuple, Anum_pg_constraint_conkey,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conkey");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != INT2OID)+ elog(ERROR, "conkey is not a 1-D smallint array");+ memcpy(conkey, ARR_DATA_PTR(arr), nelem * sizeof(AttrNumber));++ datum = fastgetattr(tuple, Anum_pg_constraint_confkey,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null confkey");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != INT2OID)+ elog(ERROR, "confkey is not a 1-D smallint array");+ memcpy(confkey, ARR_DATA_PTR(arr), nelem * sizeof(AttrNumber));++ datum = fastgetattr(tuple, Anum_pg_constraint_conpfeqop,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conpfeqop");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != OIDOID)+ elog(ERROR, "conpfeqop is not a 1-D OID array");+ memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));++ datum = fastgetattr(tuple, Anum_pg_constraint_conpfeqop,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conpfeqop");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != OIDOID)+ elog(ERROR, "conpfeqop is not a 1-D OID array");+ memcpy(conpfeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));++ datum = fastgetattr(tuple, Anum_pg_constraint_conppeqop,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conppeqop");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != OIDOID)+ elog(ERROR, "conppeqop is not a 1-D OID array");+ memcpy(conppeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));++ datum = fastgetattr(tuple, Anum_pg_constraint_conffeqop,+ tupdesc, &isnull);+ if (isnull)+ elog(ERROR, "null conffeqop");+ arr = DatumGetArrayTypeP(datum);+ nelem = ARR_DIMS(arr)[0];+ if (ARR_NDIM(arr) != 1 ||+ nelem < 1 ||+ nelem > INDEX_MAX_KEYS ||+ ARR_HASNULL(arr) ||+ ARR_ELEMTYPE(arr) != OIDOID)+ elog(ERROR, "conffeqop is not a 1-D OID array");+ memcpy(conffeqop, ARR_DATA_PTR(arr), nelem * sizeof(Oid));++ constrOid =+ CreateConstraintEntry(NameStr(constrForm->conname),+ constrForm->connamespace,+ CONSTRAINT_FOREIGN,+ constrForm->condeferrable,+ constrForm->condeferred,+ constrForm->convalidated,+ relationId,+ conkey,+ nelem,+ InvalidOid, /* not a domain constraint */+ constrForm->conindid, /* same index */+ constrForm->confrelid, /* same foreign rel */+ confkey,+ conpfeqop,+ conppeqop,+ conffeqop,+ nelem,+ constrForm->confupdtype,+ constrForm->confdeltype,+ constrForm->confmatchtype,+ NULL,+ NULL,+ NULL,+ NULL,+ false,+ 1, false, true);++ ObjectAddressSet(childAddr, ConstraintRelationId, constrOid);+ recordDependencyOn(&childAddr, &parentAddr, DEPENDENCY_INTERNAL);++ fkconstraint = makeNode(Constraint);+ /* for now this is all we need */+ fkconstraint->fk_upd_action = constrForm->confupdtype;+ fkconstraint->fk_del_action = constrForm->confdeltype;+ fkconstraint->deferrable = constrForm->condeferrable;+ fkconstraint->initdeferred = constrForm->condeferred;++ createForeignKeyTriggers(rel, constrForm->confrelid, fkconstraint,+ constrOid, constrForm->conindid);++ /*+ * XXX Normal constraint creation can be invoked during ALTER and+ * so it needs ALTER TABLE's phase 3 checking. Current caller is just+ * CREATE TABLE .. PARTITION OF so we don't need it, but maybe for+ * ALTER TABLE .. ATTACH PARTITION we'll need it.+ */+ }+ systable_endscan(scan);++ heap_close(rel, NoLock); /* keep lock till commit */+ heap_close(pg_constraint, RowShareLock);+}
/*
* Test whether given name is currently used as a constraint name
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.cindex 47cb7c13ef..e59d774a1b 100644--- a/src/backend/commands/tablecmds.c+++ b/src/backend/commands/tablecmds.c@@ -337,9 +337,6 @@ static void validateCheckConstraint(Relation rel, HeapTuple constrtup);
static void validateForeignKeyConstraint(char *conname,
Relation rel, Relation pkrel,
Oid pkindOid, Oid constraintOid);
-static void createForeignKeyTriggers(Relation rel, Oid refRelOid,- Constraint *fkconstraint,- Oid constraintOid, Oid indexOid);
static void ATController(AlterTableStmt *parsetree,
Relation rel, List *cmds, bool recurse, LOCKMODE lockmode);
static void ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
@@ -410,8 +407,10 @@ static ObjectAddress ATAddCheckConstraint(List **wqueue,
Constraint *constr,
bool recurse, bool recursing, bool is_readd,
LOCKMODE lockmode);
-static ObjectAddress ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,- Constraint *fkconstraint, LOCKMODE lockmode);+static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab,+ Relation rel,+ Constraint *fkconstraint, bool recurse, bool recursing,+ LOCKMODE lockmode);
static void ATExecDropConstraint(Relation rel, const char *constrName,
DropBehavior behavior,
bool recurse, bool recursing,
@@ -503,6 +502,7 @@ static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
* relkind: relkind to assign to the new relation
* ownerId: if not InvalidOid, use this as the new relation's owner.
* typaddress: if not null, it's set to the pg_type entry's address.
+ * queryString: for error reporting
*
* Note that permissions checks are done against current user regardless of
* ownerId. A nonzero ownerId is used when someone is creating a relation
@@ -955,6 +955,9 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
}
list_free(idxlist);
++ CloneForeignKeyConstraints(parentId, relationId);+
heap_close(parent, NoLock);
}
@@ -7034,7 +7037,8 @@ ATExecAddConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
RelationGetNamespace(rel),
NIL);
- address = ATAddForeignKeyConstraint(tab, rel, newConstraint,+ address = ATAddForeignKeyConstraint(wqueue, tab, rel,+ newConstraint, recurse, false,
lockmode);
break;
@@ -7189,8 +7193,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
* We do permissions checks here, however.
*/
static ObjectAddress
-ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,- Constraint *fkconstraint, LOCKMODE lockmode)+ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,+ Constraint *fkconstraint, bool recurse,+ bool recursing, LOCKMODE lockmode)
{
Relation pkrel;
int16 pkattnum[INDEX_MAX_KEYS];
@@ -7224,12 +7229,22 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
* numbers)
*/
if (pkrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
- ereport(ERROR,- (errcode(ERRCODE_WRONG_OBJECT_TYPE),- errmsg("cannot reference partitioned table \"%s\"",- RelationGetRelationName(pkrel))));+ {+ if (!recurse)+ ereport(ERROR,+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),+ errmsg("foreign key referencing partitioned table \"%s\" must not be ONLY",+ RelationGetRelationName(pkrel))));+ /* fix recursion in ATExecValidateConstraint to enable this case */+ if (fkconstraint->skip_validation && !fkconstraint->initially_valid)+ ereport(ERROR,+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),+ errmsg("cannot add NOT VALID foreign key to relation \"%s\"",+ RelationGetRelationName(pkrel))));+ }- if (pkrel->rd_rel->relkind != RELKIND_RELATION)+ if (pkrel->rd_rel->relkind != RELKIND_RELATION &&+ pkrel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("referenced relation \"%s\" is not a table",
@@ -7589,6 +7604,45 @@ ATAddForeignKeyConstraint(AlteredTableInfo *tab, Relation rel,
}
/*
+ * If this is a partitioned table, recurse to create the constraint on the+ * partitions also.+ */+ if (recurse)+ {+ List *children;+ ListCell *child;++ /* XXX why not find_all_inheritors? */+ children = find_inheritance_children(RelationGetRelid(rel), lockmode);++ foreach(child, children)+ {+ Oid childrelid = lfirst_oid(child);+ Relation childrel;+ AlteredTableInfo *childtab;+ ObjectAddress childAddr;++ /* find_inheritance_children already got lock */+ childrel = heap_open(childrelid, NoLock);+ CheckTableNotInUse(childrel, "ALTER TABLE"); /* XXX do we need this? */++ /* Find or create work queue entry for this table */+ childtab = ATGetQueueEntry(wqueue, childrel);++ /* Recurse to child */+ childAddr =+ ATAddForeignKeyConstraint(wqueue, childtab, childrel,+ fkconstraint, recurse, true,+ lockmode);++ /* make sure they go away together, or not at all */+ recordDependencyOn(&childAddr, &address, DEPENDENCY_INTERNAL);++ heap_close(childrel, NoLock);+ }+ }++ /*
* Close pk table, but keep lock until we've committed.
*/
heap_close(pkrel, NoLock);
@@ -7850,8 +7904,8 @@ ATExecValidateConstraint(Relation rel, char *constrName, bool recurse,
heap_close(refrel, NoLock);
/*
- * Foreign keys do not inherit, so we purposely ignore the- * recursion bit here+ * We disallow creating invalid foreign keys to or from+ * partitioned tables, so ignoring the recursion bit is okay.
*/
}
else if (con->contype == CONSTRAINT_CHECK)
@@ -8502,7 +8556,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
* NB: if you change any trigger properties here, see also
* ATExecAlterConstraint.
*/
-static void+void
createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
Oid constraintOid, Oid indexOid)
{
@@ -8632,6 +8686,25 @@ createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint,
indexOid, true);
CreateFKCheckTrigger(myRelOid, refRelOid, fkconstraint, constraintOid,
indexOid, false);
++ /*+ * If this is a partitioned table, recurse to create triggers for each+ * child. We consider that one pg_constraint entry is enough; we only+ * need the triggers to appear per-partition.+ */+ if (get_rel_relkind(refRelOid) == RELKIND_PARTITIONED_TABLE)+ {+ ListCell *cell;+ List *dchildren;++ /* XXX maybe we need a stronger lock? */+ dchildren = find_inheritance_children(refRelOid, RowShareLock);+ foreach(cell, dchildren)+ {+ createForeignKeyTriggers(rel, lfirst_oid(cell), fkconstraint,+ constraintOid, indexOid);+ }+ }
}
/*
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.cindex e80edd3668..0a8b7760d1 100644--- a/src/backend/parser/parse_utilcmd.c+++ b/src/backend/parser/parse_utilcmd.c@@ -730,12 +730,6 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
errmsg("foreign key constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("foreign key constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
/*
* Fill in the current attribute's name and throw it into the
@@ -849,12 +843,6 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
errmsg("foreign key constraints are not supported on foreign tables"),
parser_errposition(cxt->pstate,
constraint->location)));
- if (cxt->ispartitioned)- ereport(ERROR,- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),- errmsg("foreign key constraints are not supported on partitioned tables"),- parser_errposition(cxt->pstate,- constraint->location)));
cxt->fkconstraints = lappend(cxt->fkconstraints, constraint);
break;
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.cindex b1ae9e5f96..f9c0e62417 100644--- a/src/backend/utils/adt/ri_triggers.c+++ b/src/backend/utils/adt/ri_triggers.c@@ -401,7 +401,7 @@ RI_FKey_check(TriggerData *trigdata)
/* ----------
* The query string built is
- * SELECT 1 FROM ONLY <pktable> x WHERE pkatt1 = $1 [AND ...]+ * SELECT 1 FROM <pktable> x WHERE pkatt1 = $1 [AND ...]
* FOR KEY SHARE OF x
* The type id's for the $ parameters are those of the
* corresponding FK attributes.
@@ -409,7 +409,7 @@ RI_FKey_check(TriggerData *trigdata)
*/
initStringInfo(&querybuf);
quoteRelationName(pkrelname, pk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM ONLY %s x", pkrelname);+ appendStringInfo(&querybuf, "SELECT 1 FROM %s x", pkrelname);
querysep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
{
@@ -537,7 +537,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
/* ----------
* The query string built is
- * SELECT 1 FROM ONLY <pktable> x WHERE pkatt1 = $1 [AND ...]+ * SELECT 1 FROM <pktable> x WHERE pkatt1 = $1 [AND ...]
* FOR KEY SHARE OF x
* The type id's for the $ parameters are those of the
* PK attributes themselves.
@@ -545,7 +545,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel,
*/
initStringInfo(&querybuf);
quoteRelationName(pkrelname, pk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM ONLY %s x", pkrelname);+ appendStringInfo(&querybuf, "SELECT 1 FROM %s x", pkrelname);
querysep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
{
@@ -793,7 +793,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
/* ----------
* The query string built is
- * SELECT 1 FROM ONLY <fktable> x WHERE $1 = fkatt1 [AND ...]+ * SELECT 1 FROM <fktable> x WHERE $1 = fkatt1 [AND ...]
* FOR KEY SHARE OF x
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
@@ -801,7 +801,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action)
*/
initStringInfo(&querybuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "SELECT 1 FROM ONLY %s x",+ appendStringInfo(&querybuf, "SELECT 1 FROM %s x",
fkrelname);
querysep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
@@ -951,14 +951,14 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS)
/* ----------
* The query string built is
- * DELETE FROM ONLY <fktable> WHERE $1 = fkatt1 [AND ...]+ * DELETE FROM <fktable> WHERE $1 = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
* ----------
*/
initStringInfo(&querybuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "DELETE FROM ONLY %s", fkrelname);+ appendStringInfo(&querybuf, "DELETE FROM %s", fkrelname);
querysep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
{
@@ -1122,7 +1122,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
/* ----------
* The query string built is
- * UPDATE ONLY <fktable> SET fkatt1 = $1 [, ...]+ * UPDATE <fktable> SET fkatt1 = $1 [, ...]
* WHERE $n = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes. Note that we are assuming
@@ -1133,7 +1133,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS)
initStringInfo(&querybuf);
initStringInfo(&qualbuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "UPDATE ONLY %s SET", fkrelname);+ appendStringInfo(&querybuf, "UPDATE %s SET", fkrelname);
querysep = "";
qualsep = "WHERE";
for (i = 0, j = riinfo->nkeys; i < riinfo->nkeys; i++, j++)
@@ -1342,7 +1342,7 @@ ri_setnull(TriggerData *trigdata)
/* ----------
* The query string built is
- * UPDATE ONLY <fktable> SET fkatt1 = NULL [, ...]+ * UPDATE <fktable> SET fkatt1 = NULL [, ...]
* WHERE $1 = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
@@ -1351,7 +1351,7 @@ ri_setnull(TriggerData *trigdata)
initStringInfo(&querybuf);
initStringInfo(&qualbuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "UPDATE ONLY %s SET", fkrelname);+ appendStringInfo(&querybuf, "UPDATE %s SET", fkrelname);
querysep = "";
qualsep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
@@ -1559,7 +1559,7 @@ ri_setdefault(TriggerData *trigdata)
/* ----------
* The query string built is
- * UPDATE ONLY <fktable> SET fkatt1 = DEFAULT [, ...]+ * UPDATE <fktable> SET fkatt1 = DEFAULT [, ...]
* WHERE $1 = fkatt1 [AND ...]
* The type id's for the $ parameters are those of the
* corresponding PK attributes.
@@ -1568,7 +1568,7 @@ ri_setdefault(TriggerData *trigdata)
initStringInfo(&querybuf);
initStringInfo(&qualbuf);
quoteRelationName(fkrelname, fk_rel);
- appendStringInfo(&querybuf, "UPDATE ONLY %s SET", fkrelname);+ appendStringInfo(&querybuf, "UPDATE %s SET", fkrelname);
querysep = "";
qualsep = "WHERE";
for (i = 0; i < riinfo->nkeys; i++)
@@ -1895,8 +1895,8 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
/*----------
* The query string built is:
- * SELECT fk.keycols FROM ONLY relname fk- * LEFT OUTER JOIN ONLY pkrelname pk+ * SELECT fk.keycols FROM relname fk+ * LEFT OUTER JOIN pkrelname pk
* ON (pk.pkkeycol1=fk.keycol1 [AND ...])
* WHERE pk.pkkeycol1 IS NULL AND
* For MATCH SIMPLE:
@@ -1922,7 +1922,7 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel)
quoteRelationName(pkrelname, pk_rel);
quoteRelationName(fkrelname, fk_rel);
appendStringInfo(&querybuf,
- " FROM ONLY %s fk LEFT OUTER JOIN ONLY %s pk ON",+ " FROM %s fk LEFT OUTER JOIN %s pk ON",
fkrelname, pkrelname);
strcpy(pkattname, "pk.");
@@ -2345,22 +2345,6 @@ ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk)
/* Find or create a hashtable entry for the constraint */
riinfo = ri_LoadConstraintInfo(constraintOid);
- /* Do some easy cross-checks against the trigger call data */- if (rel_is_pk)- {- if (riinfo->fk_relid != trigger->tgconstrrelid ||- riinfo->pk_relid != RelationGetRelid(trig_rel))- elog(ERROR, "wrong pg_constraint entry for trigger \"%s\" on table \"%s\"",- trigger->tgname, RelationGetRelationName(trig_rel));- }- else- {- if (riinfo->fk_relid != RelationGetRelid(trig_rel) ||- riinfo->pk_relid != trigger->tgconstrrelid)- elog(ERROR, "wrong pg_constraint entry for trigger \"%s\" on table \"%s\"",- trigger->tgname, RelationGetRelationName(trig_rel));- }-
return riinfo;
}
diff --git a/src/include/catalog/pg_constraint_fn.h b/src/include/catalog/pg_constraint_fn.hindex 37b0b4ba82..438f5402fe 100644--- a/src/include/catalog/pg_constraint_fn.h+++ b/src/include/catalog/pg_constraint_fn.h@@ -56,6 +56,8 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool conNoInherit,
bool is_internal);
+extern void CloneForeignKeyConstraints(Oid parentId, Oid relationId);+
extern void RemoveConstraintById(Oid conId);
extern void RenameConstraintById(Oid conId, const char *newname);
extern void SetValidatedConstraintById(Oid conId);
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.hindex da3ff5dbee..c68515345b 100644--- a/src/include/commands/tablecmds.h+++ b/src/include/commands/tablecmds.h@@ -74,6 +74,10 @@ extern void find_composite_type_dependencies(Oid typeOid,
extern void check_of_type(HeapTuple typetuple);
+extern void createForeignKeyTriggers(Relation rel, Oid refRelOid,+ Constraint *fkconstraint, Oid constraintOid,+ Oid indexOid);+
extern void register_on_commit_action(Oid relid, OnCommitAction action);
extern void remove_on_commit_action(Oid relid);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.outindex 2caf930242..65d2058af7 100644--- a/src/test/regress/expected/alter_table.out+++ b/src/test/regress/expected/alter_table.out@@ -3290,10 +3290,6 @@ CREATE TABLE partitioned (
a int,
b int
) PARTITION BY RANGE (a, (a+b+1));
-ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;-ERROR: foreign key constraints are not supported on partitioned tables-LINE 1: ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;- ^
ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
ERROR: exclusion constraints are not supported on partitioned tables
LINE 1: ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.outindex 866cc99b9f..7c3703b73d 100644--- a/src/test/regress/expected/create_table.out+++ b/src/test/regress/expected/create_table.out@@ -276,16 +276,6 @@ CREATE TABLE partitioned (
) PARTITION BY LIST (a1, a2); -- fail
ERROR: cannot use "list" partition strategy with more than one column
-- unsupported constraint type for partitioned tables
-CREATE TABLE pkrel (- a int PRIMARY KEY-);-CREATE TABLE partitioned (- a int REFERENCES pkrel(a)-) PARTITION BY RANGE (a);-ERROR: foreign key constraints are not supported on partitioned tables-LINE 2: a int REFERENCES pkrel(a)- ^-DROP TABLE pkrel;
CREATE TABLE partitioned (
a int,
EXCLUDE USING gist (a WITH &&)
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sqlindex ed0bb7845b..3f0265b99d 100644--- a/src/test/regress/sql/alter_table.sql+++ b/src/test/regress/sql/alter_table.sql@@ -375,6 +375,54 @@ DROP TABLE tmp3;
DROP TABLE tmp2;
+-- Ensure we can add foreign keys to and from partitioned tables+SET search_path TO at_tst;+CREATE SCHEMA at_tst;+CREATE TABLE at_regular1 (col1 INT PRIMARY KEY);+CREATE TABLE at_partitioned (col2 INT PRIMARY KEY,+ col1 INT NOT NULL) PARTITION BY RANGE (col2);+CREATE TABLE at_regular2 (col2 INT);+ALTER TABLE at_regular2 ADD FOREIGN KEY (col2) REFERENCES at_partitioned;+ALTER TABLE at_partitioned ADD FOREIGN KEY (col1) REFERENCES at_regular1;+CREATE TABLE at_partitioned_0 PARTITION OF at_partitioned+ FOR VALUES FROM (0) TO (10000);+-- these fail:+INSERT INTO at_regular2 VALUES (1000);+INSERT INTO at_partitioned VALUES (1000, 42);++-- these work:+INSERT INTO at_regular1 VALUES (1000);+INSERT INTO at_partitioned VALUES (42, 1000);+INSERT INTO at_regular2 VALUES (42);++CREATE TABLE at_partitioned_1 PARTITION OF at_partitioned+ FOR VALUES FROM (10000) TO (20000);+CREATE TABLE at_partitioned_2 (col2 INT, col1 INT);+ALTER TABLE at_partitioned ATTACH PARTITION at_partitioned_2+ FOR VALUES FROM (20000) TO (30000);+ALTER TABLE at_partitioned_2 ALTER col2 SET NOT NULL;+ALTER TABLE at_partitioned ATTACH PARTITION at_partitioned_2+ FOR VALUES FROM (20000) TO (30000);++\d at_regular2+\d at_partitioned+\d at_partitioned_0+\d at_partitioned_1+\d at_partitioned_2++INSERT INTO at_partitioned VALUES (5000, 42), (15000, 1042), (25000, 2042);+INSERT INTO at_regular2 VALUES (5000, 42), (15000, 1042), (25000, 2042);+++ALTER TABLE at_regular2 DROP CONSTRAINT at_regular2_col2_fkey;+ALTER TABLE at_partitioned_0 DROP CONSTRAINT at_partitioned_col1_fkey;+ALTER TABLE at_partitioned DROP CONSTRAINT at_partitioned_col1_fkey;++\set VERBOSITY terse+DROP SCHEMA at_tst CASCADE;+\set VERBOSITY default+RESET search_path;+
-- NOT VALID with plan invalidation -- ensure we don't use a constraint for
-- exclusion until validated
set constraint_exclusion TO 'partition';
@@ -2016,7 +2064,6 @@ CREATE TABLE partitioned (
a int,
b int
) PARTITION BY RANGE (a, (a+b+1));
-ALTER TABLE partitioned ADD FOREIGN KEY (a) REFERENCES blah;
ALTER TABLE partitioned ADD EXCLUDE USING gist (a WITH &&);
-- cannot drop column that is part of the partition key
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sqlindex fefccf21a2..09a634d79d 100644--- a/src/test/regress/sql/create_table.sql+++ b/src/test/regress/sql/create_table.sql@@ -294,14 +294,6 @@ CREATE TABLE partitioned (
) PARTITION BY LIST (a1, a2); -- fail
-- unsupported constraint type for partitioned tables
-CREATE TABLE pkrel (- a int PRIMARY KEY-);-CREATE TABLE partitioned (- a int REFERENCES pkrel(a)-) PARTITION BY RANGE (a);-DROP TABLE pkrel;-
CREATE TABLE partitioned (
a int,
EXCLUDE USING gist (a WITH &&)
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sqlindex 5f19dad03c..c386e33922 100644--- a/src/test/regress/sql/foreign_key.sql+++ b/src/test/regress/sql/foreign_key.sql@@ -1055,3 +1055,33 @@ alter table fktable2 drop constraint fktable2_f1_fkey;
commit;
drop table pktable2, fktable2;
+++--+-- Foreign keys and partitioned tables+--++-- Test that it's possible to have a FK from a partitioned table to a regular+-- one+CREATE TABLE pkregular (f1 int primary key);+CREATE TABLE fkpartit (f1 int references pkregular) PARTITION BY RANGE (f1);+CREATE TABLE fkpart1 PARTITION OF fkpartit FOR VALUES FROM (0) TO (1000);+INSERT INTO fkpartit VALUES (500);+INSERT INTO fkpart1 VALUES (500);+INSERT INTO pkregular VALUES (500);+INSERT INTO fkpartit VALUES (500);+INSERT INTO fkpart1 VALUES (500);+DELETE FROM pkregular;+UPDATE pkregular SET f1 = 501;++ALTER TABLE fkpart1 DROP CONSTRAINT fkpartit_f1_fkey; -- nope+ALTER TABLE fkpartit DROP CONSTRAINT fkpartit_f1_fkey;+\d fkpartit+\d fkpart1+ALTER TABLE fkpartit ADD CONSTRAINT fkpartit_f1_fkey FOREIGN KEY (f1) REFERENCES pkregular ON DELETE CASCADE;+\d fkpartit+\d fkpart1+DELETE FROM pkregular;+SELECT * FROM fkpartit;++-- Test ALTER TABLE .. ATTACH PARTITION to clone constraints--
2.11.0
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: alvherre@alvh.no-ip.org, robertmhaas@gmail.com, david.rowley@2ndquadrant.com, simon@2ndquadrant.com, milyutinma@gmail.com
Subject: Re: [HACKERS] Proposal: Local indexes for partitioned table
In-Reply-To: <20171220194937.pldcecyx7yrwmgkg@alvherre.pgsql>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox