doc fail about ALTER TABLE ATTACH re. NO INHERIT
Hello,
While doing final review for not-null constraints, I noticed that the
ALTER TABLE ATTACH PARTITION have this phrase:
If any of the CHECK constraints of the table being attached are marked NO
INHERIT, the command will fail; such constraints must be recreated without the
NO INHERIT clause.
However, this is not true and apparently has never been true. I tried
this in both master and pg10:
create table parted (a int) partition by list (a);
create table part1 (a int , check (a > 0) no inherit);
alter table parted attach partition part1 for values in (1);
In both versions (and I imagine all intermediate ones) that sequence
works fine and results in this table:
Table "public.part1"
Column │ Type │ Collation │ Nullable │ Default │ Storage │ Stats target │ Description
────────┼─────────┼───────────┼──────────┼─────────┼─────────┼──────────────┼─────────────
a │ integer │ │ │ │ plain │ │
Partition of: parted FOR VALUES IN (1)
Partition constraint: ((a IS NOT NULL) AND (a = 1))
Check constraints:
"part1_a_check" CHECK (a > 0) NO INHERIT
On the other hand, if we were to throw an error in the ALTER TABLE as
the docs say, it would serve no purpose: the partition cannot have any
more descendants, so the fact that the CHECK constraint is NO INHERIT
makes no difference. So I think the code is fine and we should just fix
the docs.
Note that if you interpret it the other way around, i.e., that the
"table being attached" is the parent table, then the first CREATE
already fails:
create table parted2 (a int check (a > 0) no inherit) partition by list (a);
ERROR: cannot add NO INHERIT constraint to partitioned table "parted2"
This says that we don't need to worry about this condition in the parent
table either.
All in all, I think this text serves no purpose and should be removed
(from all live branches), as in the attached patch.
This text came in with the original partitioning commit f0e44751d717.
CCing Robert and Amit.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"No renuncies a nada. No te aferres a nada."
On 2024-Nov-05, Alvaro Herrera wrote:
All in all, I think this text serves no purpose and should be removed
(from all live branches), as in the attached patch.
Attached here.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Los cuentos de hadas no dan al niño su primera idea sobre los monstruos.
Lo que le dan es su primera idea de la posible derrota del monstruo."
(G. K. Chesterton)
Attachments:
0001-doc-Remove-incorrect-assertion-about-ALTER-TABLE-fai.patchtext/x-diff; charset=utf-8Download
From 718c3e17a76f4adf219f4e1d1aaf79766efaeb02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Tue, 5 Nov 2024 12:52:47 +0100
Subject: [PATCH] doc: Remove incorrect assertion about ALTER TABLE failing
---
doc/src/sgml/ref/alter_table.sgml | 4 ----
1 file changed, 4 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 36770c012a6..c054530e723 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1026,10 +1026,6 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<literal>UNIQUE</literal> and <literal>PRIMARY KEY</literal> constraints
from the parent table will be created in the partition, if they don't
already exist.
- If any of the <literal>CHECK</literal> constraints of the table being
- attached are marked <literal>NO INHERIT</literal>, the command will fail;
- such constraints must be recreated without the
- <literal>NO INHERIT</literal> clause.
</para>
<para>
--
2.39.5
Hi Alvaro,
On Tue, Nov 5, 2024 at 9:01 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Hello,
While doing final review for not-null constraints, I noticed that the
ALTER TABLE ATTACH PARTITION have this phrase:If any of the CHECK constraints of the table being attached are marked NO
INHERIT, the command will fail; such constraints must be recreated without the
NO INHERIT clause.However, this is not true and apparently has never been true. I tried
this in both master and pg10:create table parted (a int) partition by list (a);
create table part1 (a int , check (a > 0) no inherit);
alter table parted attach partition part1 for values in (1);In both versions (and I imagine all intermediate ones) that sequence
works fine and results in this table:Table "public.part1"
Column │ Type │ Collation │ Nullable │ Default │ Storage │ Stats target │ Description
────────┼─────────┼───────────┼──────────┼─────────┼─────────┼──────────────┼─────────────
a │ integer │ │ │ │ plain │ │
Partition of: parted FOR VALUES IN (1)
Partition constraint: ((a IS NOT NULL) AND (a = 1))
Check constraints:
"part1_a_check" CHECK (a > 0) NO INHERITOn the other hand, if we were to throw an error in the ALTER TABLE as
the docs say, it would serve no purpose: the partition cannot have any
more descendants, so the fact that the CHECK constraint is NO INHERIT
makes no difference. So I think the code is fine and we should just fix
the docs.Note that if you interpret it the other way around, i.e., that the
"table being attached" is the parent table, then the first CREATE
already fails:create table parted2 (a int check (a > 0) no inherit) partition by list (a);
ERROR: cannot add NO INHERIT constraint to partitioned table "parted2"This says that we don't need to worry about this condition in the parent
table either.All in all, I think this text serves no purpose and should be removed
(from all live branches), as in the attached patch.
I think that text might be talking about this case:
create table parent (a int, constraint check_a check (a > 0))
partition by list (a);
create table part1 (a int, constraint check_a check (a > 0) no inherit);
alter table parent attach partition part1 for values in (1);
ERROR: constraint "check_a" conflicts with non-inherited constraint
on child table "part1"
which is due to this code in MergeConstraintsIntoExisting():
/* If the child constraint is "no inherit" then cannot merge */
if (child_con->connoinherit)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with
non-inherited constraint on child table \"%s\"",
NameStr(child_con->conname),
RelationGetRelationName(child_rel))));
that came in with the following commit:
commit 3b11247aadf857bbcbfc765191273973d9ca9dd7
Author: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Mon Jan 16 19:19:42 2012 -0300
Disallow merging ONLY constraints in children tables
Perhaps the ATTACH PARTITION text should be changed to make clear
which case it's talking about, say, like:
If any of the CHECK constraints of the table being attached are marked
NO INHERIT, the command will fail if a constraint with the same name
exists in the parent table; such constraints must be recreated without
the NO INHERIT clause.
--
Thanks, Amit Langote
On 2024-Nov-06, Amit Langote wrote:
On Tue, Nov 5, 2024 at 9:01 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
While doing final review for not-null constraints, I noticed that the
ALTER TABLE ATTACH PARTITION have this phrase:If any of the CHECK constraints of the table being attached are marked NO
INHERIT, the command will fail; such constraints must be recreated without the
NO INHERIT clause.
I think that text might be talking about this case:
create table parent (a int, constraint check_a check (a > 0))
partition by list (a);
create table part1 (a int, constraint check_a check (a > 0) no inherit);
alter table parent attach partition part1 for values in (1);
ERROR: constraint "check_a" conflicts with non-inherited constraint on child table "part1"
Oh, hmm, that makes sense I guess. Still, while this restriction makes
sense for inheritance, it doesn't IMO for partitioned tables. I would
even suggest that we drop enforcement of this restriction during ATTACH.
Perhaps the ATTACH PARTITION text should be changed to make clear
which case it's talking about, say, like:If any of the CHECK constraints of the table being attached are marked
NO INHERIT, the command will fail if a constraint with the same name
exists in the parent table; such constraints must be recreated without
the NO INHERIT clause.
Hmm, not sure about it; I think we're giving too much prominence to a
detail that's not so important that it warrants four extra lines, when
it could be a parenthical note next to the other mention of those
constraints earlier in that paragraph. I suggest something like this:
<para>
A partition using <literal>FOR VALUES</literal> uses same syntax for
<replaceable class="parameter">partition_bound_spec</replaceable> as
<link linkend="sql-createtable"><command>CREATE TABLE</command></link>. The partition bound specification
must correspond to the partitioning strategy and partition key of the
target table. The table to be attached must have all the same columns
as the target table and no more; moreover, the column types must also
match. Also, it must have all the <literal>NOT NULL</literal> and
<literal>CHECK</literal> constraints of the target table. Currently
<literal>FOREIGN KEY</literal> constraints are not considered.
<literal>UNIQUE</literal> and <literal>PRIMARY KEY</literal> constraints
from the parent table will be created in the partition, if they don't
already exist.
If any of the <literal>CHECK</literal> constraints of the table being
attached are marked <literal>NO INHERIT</literal>, the command will fail;
such constraints must be recreated without the
<literal>NO INHERIT</literal> clause.
</para>
I suggest we change it to
<para>
A partition using <literal>FOR VALUES</literal> uses same syntax for
<replaceable class="parameter">partition_bound_spec</replaceable> as
<link linkend="sql-createtable"><command>CREATE TABLE</command></link>. The partition bound specification
must correspond to the partitioning strategy and partition key of the
target table. The table to be attached must have all the same columns
as the target table and no more; moreover, the column types must also
match. Also, it must have all the <literal>NOT NULL</literal> and
<literal>CHECK</literal> constraints of the target table, not marked
<literal>NO INHERIT</literal>. Currently
<literal>FOREIGN KEY</literal> constraints are not considered.
<literal>UNIQUE</literal> and <literal>PRIMARY KEY</literal> constraints
from the parent table will be created in the partition, if they don't
already exist.
</para>
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"You don't solve a bad join with SELECT DISTINCT" #CupsOfFail
https://twitter.com/connor_mc_d/status/1431240081726115845
On Wed, Nov 6, 2024 at 9:34 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Nov-06, Amit Langote wrote:
On Tue, Nov 5, 2024 at 9:01 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
While doing final review for not-null constraints, I noticed that the
ALTER TABLE ATTACH PARTITION have this phrase:If any of the CHECK constraints of the table being attached are marked NO
INHERIT, the command will fail; such constraints must be recreated without the
NO INHERIT clause.I think that text might be talking about this case:
create table parent (a int, constraint check_a check (a > 0))
partition by list (a);
create table part1 (a int, constraint check_a check (a > 0) no inherit);
alter table parent attach partition part1 for values in (1);
ERROR: constraint "check_a" conflicts with non-inherited constraint on child table "part1"Oh, hmm, that makes sense I guess. Still, while this restriction makes
sense for inheritance, it doesn't IMO for partitioned tables. I would
even suggest that we drop enforcement of this restriction during ATTACH.
I agree. Since leaf partitions have no children to propagate
constraints to, the NO INHERIT mark shouldn't matter. And partitioned
partitions already disallow NO INHERIT constraints as you mentioned.
Do you think we should apply something like the attached at least in
the master? I found that a similar restriction exists in the CREATE
TABLE PARTITION OF path too.
Perhaps the ATTACH PARTITION text should be changed to make clear
which case it's talking about, say, like:If any of the CHECK constraints of the table being attached are marked
NO INHERIT, the command will fail if a constraint with the same name
exists in the parent table; such constraints must be recreated without
the NO INHERIT clause.Hmm, not sure about it; I think we're giving too much prominence to a
detail that's not so important that it warrants four extra lines, when
it could be a parenthical note next to the other mention of those
constraints earlier in that paragraph. I suggest something like this:<para>
A partition using <literal>FOR VALUES</literal> uses same syntax for
<replaceable class="parameter">partition_bound_spec</replaceable> as
<link linkend="sql-createtable"><command>CREATE TABLE</command></link>. The partition bound specification
must correspond to the partitioning strategy and partition key of the
target table. The table to be attached must have all the same columns
as the target table and no more; moreover, the column types must also
match. Also, it must have all the <literal>NOT NULL</literal> and
<literal>CHECK</literal> constraints of the target table. Currently
<literal>FOREIGN KEY</literal> constraints are not considered.
<literal>UNIQUE</literal> and <literal>PRIMARY KEY</literal> constraints
from the parent table will be created in the partition, if they don't
already exist.
If any of the <literal>CHECK</literal> constraints of the table being
attached are marked <literal>NO INHERIT</literal>, the command will fail;
such constraints must be recreated without the
<literal>NO INHERIT</literal> clause.
</para>I suggest we change it to
<para>
A partition using <literal>FOR VALUES</literal> uses same syntax for
<replaceable class="parameter">partition_bound_spec</replaceable> as
<link linkend="sql-createtable"><command>CREATE TABLE</command></link>. The partition bound specification
must correspond to the partitioning strategy and partition key of the
target table. The table to be attached must have all the same columns
as the target table and no more; moreover, the column types must also
match. Also, it must have all the <literal>NOT NULL</literal> and
<literal>CHECK</literal> constraints of the target table, not marked
<literal>NO INHERIT</literal>. Currently
<literal>FOREIGN KEY</literal> constraints are not considered.
<literal>UNIQUE</literal> and <literal>PRIMARY KEY</literal> constraints
from the parent table will be created in the partition, if they don't
already exist.
</para>
+1
Though if we decide to apply the attached, does the note "not marked
NO INHERIT" become unnecessary?
--
Thanks, Amit Langote
Attachments:
v1-0001-Allow-inherited-NO-INHERIT-check-constraints-in-l.patchapplication/octet-stream; name=v1-0001-Allow-inherited-NO-INHERIT-check-constraints-in-l.patchDownload
From af738a2f975e36a082218b31f9f73e84540921cf Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Thu, 7 Nov 2024 18:48:37 +0900
Subject: [PATCH v1] Allow inherited NO INHERIT check constraints in leaf
partitions
Preventing inherited NO INHERIT constraints is crucial for regular
inheritance child tables, as the NO INHERIT marking would block these
constraints from propagating to grandchildren.
Since leaf partitions cannot have children, allowing inherited NO
INHERIT constraints on them is harmless.
Reported-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Discussion: https://postgr.es/m/202411051201.zody6mld7vkw@alvherre.pgsql
---
src/backend/catalog/heap.c | 17 +++++++++++----
src/backend/commands/tablecmds.c | 18 +++++++++++-----
src/test/regress/expected/alter_table.out | 25 +++++++++++++++++++++++
src/test/regress/sql/alter_table.sql | 22 ++++++++++++++++++++
4 files changed, 73 insertions(+), 9 deletions(-)
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index c54a543c53..efbe4b3f20 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2576,8 +2576,14 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
errmsg("constraint \"%s\" for relation \"%s\" already exists",
ccname, RelationGetRelationName(rel))));
- /* If the child constraint is "no inherit" then cannot merge */
- if (con->connoinherit)
+ /*
+ * If the child constraint is "no inherit" then cannot merge because
+ * that would prevent lower-level children from inheriting it. That's
+ * not a concern if the child table is a leaf partition which cannot
+ * have child tables, so we allow the merge.
+ */
+ if (con->connoinherit && !rel->rd_rel->relispartition &&
+ !RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with non-inherited constraint on relation \"%s\"",
@@ -2586,9 +2592,12 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
/*
* Must not change an existing inherited constraint to "no inherit"
* status. That's because inherited constraints should be able to
- * propagate to lower-level children.
+ * propagate to lower-level children. Again, it's fine to allow doing
+ * so for leaf partitions.
*/
- if (con->coninhcount > 0 && is_no_inherit)
+ if (con->coninhcount > 0 && is_no_inherit &&
+ (!rel->rd_rel->relispartition ||
+ RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind)))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with inherited constraint on relation \"%s\"",
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4345b96de5..f4d581445b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -376,7 +376,8 @@ static List *MergeCheckConstraint(List *constraints, const char *name, Node *exp
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
-static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
+static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel,
+ bool child_is_partition);
static void StoreCatalogInheritance(Oid relationId, List *supers,
bool child_is_partition);
static void StoreCatalogInheritance1(Oid relationId, Oid parentOid,
@@ -15899,7 +15900,7 @@ CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition)
MergeAttributesIntoExisting(child_rel, parent_rel, ispartition);
/* Match up the constraints and bump coninhcount as needed */
- MergeConstraintsIntoExisting(child_rel, parent_rel);
+ MergeConstraintsIntoExisting(child_rel, parent_rel, ispartition);
/*
* OK, it looks valid. Make the catalog entries that show inheritance.
@@ -16094,7 +16095,8 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
* XXX See MergeWithExistingConstraint too if you change this code.
*/
static void
-MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
+MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel,
+ bool child_is_partition)
{
Relation constraintrel;
SysScanDesc parent_scan;
@@ -16153,8 +16155,14 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
errmsg("child table \"%s\" has different definition for check constraint \"%s\"",
RelationGetRelationName(child_rel), NameStr(parent_con->conname))));
- /* If the child constraint is "no inherit" then cannot merge */
- if (child_con->connoinherit)
+ /*
+ * If the child constraint is "no inherit" then cannot merge
+ * because that would prevent lower-level children from inheriting
+ * it. That's not a concern if the child table is a leaf partition
+ * which cannot have child tables, so we allow the merge.
+ */
+ if (child_con->connoinherit && !child_is_partition &&
+ !RELKIND_HAS_PARTITIONS(child_rel->rd_rel->relkind))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with non-inherited constraint on child table \"%s\"",
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 143ae7c09c..5f5fb17ae5 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -4275,6 +4275,31 @@ ALTER TABLE hash_parted ATTACH PARTITION fail_part FOR VALUES WITH (MODULUS 3, R
ERROR: every hash partition modulus must be a factor of the next larger modulus
DETAIL: The new modulus 3 is not a factor of 4, the modulus of existing partition "hpart_1".
DROP TABLE fail_part;
+-- Prevent marking a constraint as NO INHERIT on a child table when it
+-- inherits a constraint with the same name from its parent, to ensure that
+-- the grandchildren consistently inherit the constraint.
+CREATE TABLE parent_with_check (a int, CONSTRAINT check_a CHECK (a > 0));
+CREATE TABLE child1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE child1_with_noinherit_check INHERIT parent_with_check;
+ERROR: constraint "check_a" conflicts with non-inherited constraint on child table "child1_with_noinherit_check"
+CREATE TABLE child2_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT) INHERITS (parent_with_check);
+NOTICE: merging column "a" with inherited definition
+ERROR: constraint "check_a" conflicts with inherited constraint on relation "child2_with_noinherit_check"
+DROP TABLE parent_with_check, child1_with_noinherit_check;
+-- No need to have that restriction for leaf partitions though, because they
+-- cannot have any children of their own
+CREATE TABLE parted_parent_with_check (a int, CONSTRAINT check_a CHECK (a > 0)) PARTITION BY LIST (a);
+-- Case 1: ALTER TABLE parent ATTACH PARTITION leaf_partition ...
+CREATE TABLE part1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE parted_parent_with_check ATTACH PARTITION part1_with_noinherit_check FOR VALUES IN (1); -- ok
+-- Case 2: CREATE TABLE leaf_partition PARTITION OF parent ...
+CREATE TABLE part2_with_noinherit_check PARTITION OF parted_parent_with_check (CONSTRAINT check_a CHECK (a > 0) NO INHERIT) FOR VALUES IN (2); -- ok
+NOTICE: merging constraint "check_a" with inherited definition
+-- Case 3: CREATE TABLE non_leaf_partition PARTITION OF parent ... PARTITION BY
+CREATE TABLE part3_with_noinherit_check PARTITION OF parted_parent_with_check (CONSTRAINT check_a CHECK (a > 0) NO INHERIT) FOR VALUES IN (3)
+ PARTITION BY LIST (a); -- not ok
+ERROR: constraint "check_a" conflicts with inherited constraint on relation "part3_with_noinherit_check"
+DROP TABLE parted_parent_with_check;
--
-- DETACH PARTITION
--
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index c5dd43a15c..12f807e466 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2733,6 +2733,28 @@ ALTER TABLE hash_parted ATTACH PARTITION fail_part FOR VALUES WITH (MODULUS 8, R
ALTER TABLE hash_parted ATTACH PARTITION fail_part FOR VALUES WITH (MODULUS 3, REMAINDER 2);
DROP TABLE fail_part;
+-- Prevent marking a constraint as NO INHERIT on a child table when it
+-- inherits a constraint with the same name from its parent, to ensure that
+-- the grandchildren consistently inherit the constraint.
+CREATE TABLE parent_with_check (a int, CONSTRAINT check_a CHECK (a > 0));
+CREATE TABLE child1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE child1_with_noinherit_check INHERIT parent_with_check;
+CREATE TABLE child2_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT) INHERITS (parent_with_check);
+DROP TABLE parent_with_check, child1_with_noinherit_check;
+
+-- No need to have that restriction for leaf partitions though, because they
+-- cannot have any children of their own
+CREATE TABLE parted_parent_with_check (a int, CONSTRAINT check_a CHECK (a > 0)) PARTITION BY LIST (a);
+-- Case 1: ALTER TABLE parent ATTACH PARTITION leaf_partition ...
+CREATE TABLE part1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE parted_parent_with_check ATTACH PARTITION part1_with_noinherit_check FOR VALUES IN (1); -- ok
+-- Case 2: CREATE TABLE leaf_partition PARTITION OF parent ...
+CREATE TABLE part2_with_noinherit_check PARTITION OF parted_parent_with_check (CONSTRAINT check_a CHECK (a > 0) NO INHERIT) FOR VALUES IN (2); -- ok
+-- Case 3: CREATE TABLE non_leaf_partition PARTITION OF parent ... PARTITION BY
+CREATE TABLE part3_with_noinherit_check PARTITION OF parted_parent_with_check (CONSTRAINT check_a CHECK (a > 0) NO INHERIT) FOR VALUES IN (3)
+ PARTITION BY LIST (a); -- not ok
+DROP TABLE parted_parent_with_check;
+
--
-- DETACH PARTITION
--
--
2.43.0
On 2024-Nov-07, Amit Langote wrote:
On Wed, Nov 6, 2024 at 9:34 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Oh, hmm, that makes sense I guess. Still, while this restriction makes
sense for inheritance, it doesn't IMO for partitioned tables. I would
even suggest that we drop enforcement of this restriction during ATTACH.I agree. Since leaf partitions have no children to propagate
constraints to, the NO INHERIT mark shouldn't matter. And partitioned
partitions already disallow NO INHERIT constraints as you mentioned.Do you think we should apply something like the attached at least in
the master? I found that a similar restriction exists in the CREATE
TABLE PARTITION OF path too.
Yeah, that sounds reasonable. I didn't look at the code in detail, but
I'm not sure I understand why you'd change CREATE TABLE PARTITION OF,
since the point is that this restriction would apply when you attach a
table that already exists, not when you create a new table. Maybe I
misunderstand what you're saying though.
+1
Thanks, pushed.
Though if we decide to apply the attached, does the note "not marked
NO INHERIT" become unnecessary?
Yes -- I think your patch would have to remove it again. A short-lived
note for sure, but I thought it was better to have all branches in the
same state, and now you can modify master.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
On Fri, Nov 8, 2024 at 2:54 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Nov-07, Amit Langote wrote:
On Wed, Nov 6, 2024 at 9:34 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Oh, hmm, that makes sense I guess. Still, while this restriction makes
sense for inheritance, it doesn't IMO for partitioned tables. I would
even suggest that we drop enforcement of this restriction during ATTACH.I agree. Since leaf partitions have no children to propagate
constraints to, the NO INHERIT mark shouldn't matter. And partitioned
partitions already disallow NO INHERIT constraints as you mentioned.Do you think we should apply something like the attached at least in
the master? I found that a similar restriction exists in the CREATE
TABLE PARTITION OF path too.Yeah, that sounds reasonable. I didn't look at the code in detail, but
I'm not sure I understand why you'd change CREATE TABLE PARTITION OF,
since the point is that this restriction would apply when you attach a
table that already exists, not when you create a new table. Maybe I
misunderstand what you're saying though.
The restriction also exists in the CREATE TABLE PARTITION OF path:
create table parted_parent (a int, constraint check_a check (a > 0))
partition by list (a);
-- leaf partition
create table parted_parent_part1 partition of parted_parent
(constraint check_a check(a > 0) no inherit) for values in (1);
ERROR: constraint "check_a" conflicts with inherited constraint on
relation "parted_parent_part1"
-- non-leaf partition
postgres=# create table parted_parent_part1 partition of parted_parent
(constraint check_a check(a > 0) no inherit) for values in (1)
partition by list (a);
ERROR: constraint "check_a" conflicts with inherited constraint on
relation "parted_parent_part1"
I think we can remove the restriction at least for the leaf partition
case just like in the ATTACH PARTITION path.
Though if we decide to apply the attached, does the note "not marked
NO INHERIT" become unnecessary?Yes -- I think your patch would have to remove it again. A short-lived
note for sure, but I thought it was better to have all branches in the
same state, and now you can modify master.
Ok, got it.
--
Thanks, Amit Langote
On 2024-Nov-11, Amit Langote wrote:
The restriction also exists in the CREATE TABLE PARTITION OF path:
[...]
I think we can remove the restriction at least for the leaf partition
case just like in the ATTACH PARTITION path.
Sure, WFM.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
On Tue, Nov 12, 2024 at 5:46 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Nov-11, Amit Langote wrote:
The restriction also exists in the CREATE TABLE PARTITION OF path:
[...]
I think we can remove the restriction at least for the leaf partition
case just like in the ATTACH PARTITION path.Sure, WFM.
I rebased my patch over 14e87ffa5c54 and noticed that NOT NULL
constraints can also (or not) be marked NO INHERIT. I think we should
allow NO INHERIT NOT NULL constraints on leaf partitions just like
CHECK constraints, so changed AddRelationNotNullConstraints() that way
and added some tests.
However, I noticed that there is no clean way to do that for the following case:
ALTER TABLE leaf_partition ADD CONSTRAINT col_nn_ni NOT NULL col NO INHERIT;
If I change the new function AdjustNotNullInheritance() added by your
commit to not throw an error for leaf partitions, the above constraint
(col_nn_ni) is not stored at all, because the function returns true in
that case, which means the caller does nothing with the constraint. I
am not sure what the right thing to do would be. If we return false,
the caller will store the constraint the first time around, but
repeating the command will again do nothing, not even prevent the
addition of a duplicate constraint.
--
Thanks, Amit Langote
Attachments:
v2-0001-Allow-inherited-constraints-to-be-marked-NO-INHER.patchapplication/octet-stream; name=v2-0001-Allow-inherited-constraints-to-be-marked-NO-INHER.patchDownload
From d62fa6af0c800ac12ba395a183fbefbea1942184 Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Wed, 13 Nov 2024 18:13:56 +0900
Subject: [PATCH v2 1/2] Allow inherited constraints to be marked NO INHERIT in
leaf partitions
For inheritance child tables, inherited constraints can't be marked
NO INHERIT, as that would prevent their propagation to grandchildren.
Since leaf partitions have no children, marking their inherited
constraints as NO INHERIT is harmless.
This change applies to both CHECK constraints and NOT NULL
constraints, the latter of which has been allowed to be marked NO
INHERIT in leaf partitions since commit 14e87ffa5c54.
A test added in commit 14e87ffa5c54 involving a leaf partition has
been updated to reflect this change.
Reported-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Author: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Discussion: https://postgr.es/m/202411051201.zody6mld7vkw@alvherre.pgsql
---
doc/src/sgml/ref/alter_table.sgml | 3 +-
src/backend/catalog/heap.c | 27 ++++++++---
src/backend/commands/tablecmds.c | 15 +++++--
src/test/regress/expected/alter_table.out | 29 ++++++++----
src/test/regress/expected/create_table.out | 51 +++++++++++++++++++++
src/test/regress/sql/alter_table.sql | 29 ++++++++----
src/test/regress/sql/create_table.sql | 52 ++++++++++++++++++++++
7 files changed, 178 insertions(+), 28 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 6098ebed43..24c027600c 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1020,8 +1020,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
target table. The table to be attached must have all the same columns
as the target table and no more; moreover, the column types must also
match. Also, it must have all the <literal>NOT NULL</literal> and
- <literal>CHECK</literal> constraints of the target table, not marked
- <literal>NO INHERIT</literal>. Currently
+ <literal>CHECK</literal> constraints of the target table. Currently
<literal>FOREIGN KEY</literal> constraints are not considered.
<literal>UNIQUE</literal> and <literal>PRIMARY KEY</literal> constraints
from the parent table will be created in the partition, if they don't
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 003af4bf21..e3aca9f418 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2707,8 +2707,15 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
errmsg("constraint \"%s\" for relation \"%s\" already exists",
ccname, RelationGetRelationName(rel))));
- /* If the child constraint is "no inherit" then cannot merge */
- if (con->connoinherit)
+ /*
+ * If the child constraint is "no inherit" then cannot merge because
+ * that would prevent lower-level children from inheriting it. That's
+ * not a concern if the child table is a leaf partition which cannot
+ * have child tables, so we allow the merge.
+ */
+ if (con->connoinherit &&
+ (!rel->rd_rel->relispartition ||
+ RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind)))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with non-inherited constraint on relation \"%s\"",
@@ -2717,9 +2724,12 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
/*
* Must not change an existing inherited constraint to "no inherit"
* status. That's because inherited constraints should be able to
- * propagate to lower-level children.
+ * propagate to lower-level children. It's fine to allow doing so for
+ * leaf partitions, as described above.
*/
- if (con->coninhcount > 0 && is_no_inherit)
+ if (con->coninhcount > 0 && is_no_inherit &&
+ (!rel->rd_rel->relispartition ||
+ RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind)))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with inherited constraint on relation \"%s\"",
@@ -2904,9 +2914,14 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
{
/*
* If we get a constraint from the parent, having a local NO
- * INHERIT one doesn't work.
+ * INHERIT one doesn't work, because the constraint inherited
+ * from the parent won't be propagated to grand children.
+ * That's not a concern if the child table is a leaf partition
+ * which cannot have child tables, so we allow it to have
+ * NO INHERIT constraints.
*/
- if (constr->is_no_inherit)
+ if (constr->is_no_inherit &&
+ !rel->rd_rel->relispartition)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("cannot define not-null constraint on column \"%s\" with NO INHERIT",
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ccd9645e7d..84b6617539 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -377,7 +377,8 @@ static List *MergeCheckConstraint(List *constraints, const char *name, Node *exp
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
-static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
+static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel,
+ bool child_is_partition);
static void StoreCatalogInheritance(Oid relationId, List *supers,
bool child_is_partition);
static void StoreCatalogInheritance1(Oid relationId, Oid parentOid,
@@ -16153,7 +16154,7 @@ CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition)
MergeAttributesIntoExisting(child_rel, parent_rel, ispartition);
/* Match up the constraints and bump coninhcount as needed */
- MergeConstraintsIntoExisting(child_rel, parent_rel);
+ MergeConstraintsIntoExisting(child_rel, parent_rel, ispartition);
/*
* OK, it looks valid. Make the catalog entries that show inheritance.
@@ -16358,7 +16359,8 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
* XXX See MergeWithExistingConstraint too if you change this code.
*/
static void
-MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
+MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel,
+ bool child_is_partition)
{
Relation constraintrel;
SysScanDesc parent_scan;
@@ -16455,8 +16457,13 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
/*
* If the child constraint is "no inherit" then cannot merge
+ * because that would prevent lower-level children from inheriting
+ * it. That's not a concern if the child table is a leaf partition
+ * which cannot have child tables, so we allow the merge.
*/
- if (child_con->connoinherit)
+ if (child_con->connoinherit &&
+ (!child_is_partition ||
+ RELKIND_HAS_PARTITIONS(child_rel->rd_rel->relkind)))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with non-inherited constraint on child table \"%s\"",
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 2212c8dbb5..9cfa7257ee 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -4026,14 +4026,6 @@ SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_1'::reg
f | 1
(1 row)
--- check that NOT NULL NO INHERIT cannot be merged to a normal NOT NULL
-CREATE TABLE part_fail (a int NOT NULL NO INHERIT,
- b char(2) COLLATE "C",
- CONSTRAINT check_a CHECK (a > 0)
-);
-ALTER TABLE list_parted ATTACH PARTITION part_fail FOR VALUES IN (2);
-ERROR: constraint "part_fail_a_not_null" conflicts with non-inherited constraint on child table "part_fail"
-DROP TABLE part_fail;
-- check that the new partition won't overlap with an existing partition
CREATE TABLE fail_part (LIKE part_1 INCLUDING CONSTRAINTS);
ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1);
@@ -4282,6 +4274,27 @@ ERROR: every hash partition modulus must be a factor of the next larger modulus
DETAIL: The new modulus 3 is not a factor of 4, the modulus of existing partition "hpart_1".
DROP TABLE fail_part;
--
+-- Prevent marking a constraint as NO INHERIT on a child table when it
+-- inherits a constraint with the same name from its parent, to ensure that
+-- the grandchildren consistently inherit the constraint.
+--
+CREATE TABLE parent_with_check_notnull (a int NOT NULL, CONSTRAINT check_a CHECK (a > 0));
+CREATE TABLE child1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE child1_with_noinherit_check INHERIT parent_with_check_notnull; -- not ok
+ERROR: column "a" in child table "child1_with_noinherit_check" must be marked NOT NULL
+CREATE TABLE child2_with_noinherit_notnull (a int NOT NULL NO INHERIT, CONSTRAINT check_a CHECK (a > 0));
+ALTER TABLE child2_with_noinherit_notnull INHERIT parent_with_check_notnull; -- not ok
+ERROR: constraint "child2_with_noinherit_notnull_a_not_null" conflicts with non-inherited constraint on child table "child2_with_noinherit_notnull"
+DROP TABLE parent_with_check_notnull, child1_with_noinherit_check, child2_with_noinherit_notnull;
+-- No need to have that restriction for leaf partitions though, because they
+-- cannot have any children of their own
+CREATE TABLE parted_parent_with_check_notnull (a int, CONSTRAINT check_a CHECK (a > 0)) PARTITION BY LIST (a);
+CREATE TABLE part1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE parted_parent_with_check_notnull ATTACH PARTITION part1_with_noinherit_check FOR VALUES IN (1); -- ok
+CREATE TABLE part2_with_noinherit_notnull (a int NOT NULL NO INHERIT, CONSTRAINT check_a CHECK (a > 0));
+ALTER TABLE parted_parent_with_check_notnull ATTACH PARTITION part2_with_noinherit_notnull FOR VALUES IN (2); -- ok
+DROP TABLE parted_parent_with_check_notnull;
+--
-- DETACH PARTITION
--
-- check that the table is partitioned at all
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 76604705a9..bfe2d5ab99 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -257,6 +257,57 @@ CREATE TABLE partitioned (
CONSTRAINT check_a CHECK (a > 0) NO INHERIT
) PARTITION BY RANGE (a);
ERROR: cannot add NO INHERIT constraint to partitioned table "partitioned"
+-- also when creating the partitioned table as partition
+CREATE TABLE partitioned_with_not_null (
+ a int NOT NULL
+) PARTITION BY RANGE (a);
+CREATE TABLE partitioned_with_not_null_part1
+ PARTITION OF partitioned_with_not_null (
+ a NOT NULL NO INHERIT
+) FOR VALUES FROM (1) TO (2)
+PARTITION BY LIST (a); -- not ok
+ERROR: not-null constraints on partitioned tables cannot be NO INHERIT
+-- fine if the partition is a leaf partition though
+CREATE TABLE partitioned_with_not_null_part1
+ PARTITION OF partitioned_with_not_null (
+ a NOT NULL NO INHERIT
+) FOR VALUES FROM (1) TO (2); -- ok
+-- test cases with check constraint
+CREATE TABLE partitioned_with_check (
+ a int NOT NULL,
+ CONSTRAINT check_a CHECK (a > 0)
+) PARTITION BY RANGE (a);
+CREATE TABLE partitioned_with_check_part1
+ PARTITION OF partitioned_with_check (
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) FOR VALUES FROM (1) TO (2)
+PARTITION BY LIST (a); -- not ok
+ERROR: constraint "check_a" conflicts with inherited constraint on relation "partitioned_with_check_part1"
+-- fine if the partition is a leaf partition though
+CREATE TABLE partitioned_with_check_part1
+ PARTITION OF partitioned_with_check (
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) FOR VALUES FROM (1) TO (2); -- ok
+NOTICE: merging constraint "check_a" with inherited definition
+-- similar tests for plain inheritance
+CREATE TABLE ct_parent_with_check_notnull (
+ a int NOT NULL,
+ CONSTRAINT check_a CHECK (a > 0));
+CREATE TABLE ct_child1_with_noinherit_check (
+ a int,
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) INHERITS (ct_parent_with_check_notnull); -- not ok
+NOTICE: merging column "a" with inherited definition
+ERROR: constraint "check_a" conflicts with inherited constraint on relation "ct_child1_with_noinherit_check"
+CREATE TABLE ct_child2_with_noinherit_not_null (
+ a int NOT NULL NO INHERIT,
+ CONSTRAINT check_a CHECK (a > 0)
+) INHERITS (ct_parent_with_check_notnull); -- not ok
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging constraint "check_a" with inherited definition
+ERROR: cannot define not-null constraint on column "a" with NO INHERIT
+DETAIL: The column has an inherited not-null constraint.
+DROP TABLE partitioned_with_not_null, partitioned_with_check, ct_parent_with_check_notnull;
-- some checks after successful creation of a partitioned table
CREATE FUNCTION plusone(a int) RETURNS INT AS $$ SELECT a+1; $$ LANGUAGE SQL;
CREATE TABLE partitioned (
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 637e3dac38..8a3f8b140b 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2484,14 +2484,6 @@ ALTER TABLE list_parted ATTACH PARTITION part_1 FOR VALUES IN (1);
SELECT attislocal, attinhcount FROM pg_attribute WHERE attrelid = 'part_1'::regclass AND attnum > 0;
SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_1'::regclass AND conname = 'check_a';
--- check that NOT NULL NO INHERIT cannot be merged to a normal NOT NULL
-CREATE TABLE part_fail (a int NOT NULL NO INHERIT,
- b char(2) COLLATE "C",
- CONSTRAINT check_a CHECK (a > 0)
-);
-ALTER TABLE list_parted ATTACH PARTITION part_fail FOR VALUES IN (2);
-DROP TABLE part_fail;
-
-- check that the new partition won't overlap with an existing partition
CREATE TABLE fail_part (LIKE part_1 INCLUDING CONSTRAINTS);
ALTER TABLE list_parted ATTACH PARTITION fail_part FOR VALUES IN (1);
@@ -2738,6 +2730,27 @@ ALTER TABLE hash_parted ATTACH PARTITION fail_part FOR VALUES WITH (MODULUS 8, R
ALTER TABLE hash_parted ATTACH PARTITION fail_part FOR VALUES WITH (MODULUS 3, REMAINDER 2);
DROP TABLE fail_part;
+--
+-- Prevent marking a constraint as NO INHERIT on a child table when it
+-- inherits a constraint with the same name from its parent, to ensure that
+-- the grandchildren consistently inherit the constraint.
+--
+CREATE TABLE parent_with_check_notnull (a int NOT NULL, CONSTRAINT check_a CHECK (a > 0));
+CREATE TABLE child1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE child1_with_noinherit_check INHERIT parent_with_check_notnull; -- not ok
+CREATE TABLE child2_with_noinherit_notnull (a int NOT NULL NO INHERIT, CONSTRAINT check_a CHECK (a > 0));
+ALTER TABLE child2_with_noinherit_notnull INHERIT parent_with_check_notnull; -- not ok
+DROP TABLE parent_with_check_notnull, child1_with_noinherit_check, child2_with_noinherit_notnull;
+
+-- No need to have that restriction for leaf partitions though, because they
+-- cannot have any children of their own
+CREATE TABLE parted_parent_with_check_notnull (a int, CONSTRAINT check_a CHECK (a > 0)) PARTITION BY LIST (a);
+CREATE TABLE part1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE parted_parent_with_check_notnull ATTACH PARTITION part1_with_noinherit_check FOR VALUES IN (1); -- ok
+CREATE TABLE part2_with_noinherit_notnull (a int NOT NULL NO INHERIT, CONSTRAINT check_a CHECK (a > 0));
+ALTER TABLE parted_parent_with_check_notnull ATTACH PARTITION part2_with_noinherit_notnull FOR VALUES IN (2); -- ok
+DROP TABLE parted_parent_with_check_notnull;
+
--
-- DETACH PARTITION
--
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index 37a227148e..ffc73e292d 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -194,6 +194,58 @@ CREATE TABLE partitioned (
CONSTRAINT check_a CHECK (a > 0) NO INHERIT
) PARTITION BY RANGE (a);
+-- also when creating the partitioned table as partition
+CREATE TABLE partitioned_with_not_null (
+ a int NOT NULL
+) PARTITION BY RANGE (a);
+
+CREATE TABLE partitioned_with_not_null_part1
+ PARTITION OF partitioned_with_not_null (
+ a NOT NULL NO INHERIT
+) FOR VALUES FROM (1) TO (2)
+PARTITION BY LIST (a); -- not ok
+
+-- fine if the partition is a leaf partition though
+CREATE TABLE partitioned_with_not_null_part1
+ PARTITION OF partitioned_with_not_null (
+ a NOT NULL NO INHERIT
+) FOR VALUES FROM (1) TO (2); -- ok
+
+-- test cases with check constraint
+CREATE TABLE partitioned_with_check (
+ a int NOT NULL,
+ CONSTRAINT check_a CHECK (a > 0)
+) PARTITION BY RANGE (a);
+
+CREATE TABLE partitioned_with_check_part1
+ PARTITION OF partitioned_with_check (
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) FOR VALUES FROM (1) TO (2)
+PARTITION BY LIST (a); -- not ok
+
+-- fine if the partition is a leaf partition though
+CREATE TABLE partitioned_with_check_part1
+ PARTITION OF partitioned_with_check (
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) FOR VALUES FROM (1) TO (2); -- ok
+
+-- similar tests for plain inheritance
+CREATE TABLE ct_parent_with_check_notnull (
+ a int NOT NULL,
+ CONSTRAINT check_a CHECK (a > 0));
+
+CREATE TABLE ct_child1_with_noinherit_check (
+ a int,
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) INHERITS (ct_parent_with_check_notnull); -- not ok
+
+CREATE TABLE ct_child2_with_noinherit_not_null (
+ a int NOT NULL NO INHERIT,
+ CONSTRAINT check_a CHECK (a > 0)
+) INHERITS (ct_parent_with_check_notnull); -- not ok
+
+DROP TABLE partitioned_with_not_null, partitioned_with_check, ct_parent_with_check_notnull;
+
-- some checks after successful creation of a partitioned table
CREATE FUNCTION plusone(a int) RETURNS INT AS $$ SELECT a+1; $$ LANGUAGE SQL;
--
2.43.0
On 2024-Nov-13, Amit Langote wrote:
I rebased my patch over 14e87ffa5c54 and noticed that NOT NULL
constraints can also (or not) be marked NO INHERIT. I think we should
allow NO INHERIT NOT NULL constraints on leaf partitions just like
CHECK constraints, so changed AddRelationNotNullConstraints() that way
and added some tests.
OK, looks good.
However, I noticed that there is no clean way to do that for the following case:
ALTER TABLE leaf_partition ADD CONSTRAINT col_nn_ni NOT NULL col NO INHERIT;
Sorry, I didn't understand what's the initial state. Does the
constraint already exist here or not?
If I change the new function AdjustNotNullInheritance() added by your
commit to not throw an error for leaf partitions, the above constraint
(col_nn_ni) is not stored at all, because the function returns true in
that case, which means the caller does nothing with the constraint. I
am not sure what the right thing to do would be. If we return false,
the caller will store the constraint the first time around, but
repeating the command will again do nothing, not even prevent the
addition of a duplicate constraint.
Do you mean if we return false, it allows two not-null constraints for
the same column? That would absolutely be a bug.
I think:
* if a leaf partition already has an inherited not-null constraint
from its parent and we want to add another one, we should:
- if the one being added is NO INHERIT, then throw an error, because
we cannot merge them
- if the one being added is not NO INHERIT, then both constraints
would have the same state and so we silently do nothing.
* if a leaf partition has a locally defined not-null marked NO INHERIT
- if we add another NO INHERIT, silently do nothing
- if we add an INHERIT one, throw an error because cannot merge.
If you want, you could leave the not-null constraint case alone and I
can have a look later. It wasn't my intention to burden you with that.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
<Schwern> It does it in a really, really complicated way
<crab> why does it need to be complicated?
<Schwern> Because it's MakeMaker.
On Wed, Nov 13, 2024 at 10:49 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Nov-13, Amit Langote wrote:
I rebased my patch over 14e87ffa5c54 and noticed that NOT NULL
constraints can also (or not) be marked NO INHERIT. I think we should
allow NO INHERIT NOT NULL constraints on leaf partitions just like
CHECK constraints, so changed AddRelationNotNullConstraints() that way
and added some tests.OK, looks good.
Thanks. I'll hold off pushing until we have some clarity on whether
the behavior should be identical for CHECK and NOT NULL constraints.
For now, I have removed the changes in my patch pertaining to NOT NULL
constraints.
However, I noticed that there is no clean way to do that for the following case:
ALTER TABLE leaf_partition ADD CONSTRAINT col_nn_ni NOT NULL col NO INHERIT;
Sorry, I didn't understand what's the initial state. Does the
constraint already exist here or not?
Sorry, here's the full example. Note I'd changed both
AddRelationNotNullConstraints() and AdjustNotNullInheritance() to not
throw an error *if* the table is a leaf partition when the NO INHERIT
of an existing constraint doesn't match that of the new constraint.
create table p (a int not null) partition by list (a);
create table p1 partition of p for values in (1);
alter table p1 add constraint a_nn_ni not null a no inherit;
\d+ p1
Table "public.p1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
a | integer | | not null | | plain |
| |
Partition of: p FOR VALUES IN (1)
Partition constraint: ((a IS NOT NULL) AND (a = 1))
Not-null constraints:
"p_a_not_null" NOT NULL "a" (local, inherited)
Access method: heap
-- noop
alter table p1 add constraint a_nn_ni not null a no inherit;
alter table p1 add constraint a_nn_ni not null a no inherit;
\d+ p1
Table "public.p1"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
a | integer | | not null | | plain |
| |
Partition of: p FOR VALUES IN (1)
Partition constraint: ((a IS NOT NULL) AND (a = 1))
Not-null constraints:
"p_a_not_null" NOT NULL "a" (local, inherited)
Access method: heap
The state of the existing inherited constraint is not valid and the
a_nn_ni is never created.
So, it seems that allowing NO INHERIT NOT NULL constraints of leaf
partitions to be merged with the inherited one is not as
straightforward as it is for CHECK constraints.
If I change the new function AdjustNotNullInheritance() added by your
commit to not throw an error for leaf partitions, the above constraint
(col_nn_ni) is not stored at all, because the function returns true in
that case, which means the caller does nothing with the constraint. I
am not sure what the right thing to do would be. If we return false,
the caller will store the constraint the first time around, but
repeating the command will again do nothing, not even prevent the
addition of a duplicate constraint.Do you mean if we return false, it allows two not-null constraints for
the same column? That would absolutely be a bug.
I don't think there is any such bug at the moment, because if
AdjustNotNullInheritance() finds an existing constraint, it always
returns true provided the new constraint does not cause the error due
to its NO INHERIT property not matching the existing one's.
I think:
* if a leaf partition already has an inherited not-null constraint
from its parent and we want to add another one, we should:
- if the one being added is NO INHERIT, then throw an error, because
we cannot merge them
Hmm, we'll be doing something else for CHECK constraints if we apply my patch:
create table p (a int not null, constraint check_a check (a > 0))
partition by list (a);
create table p1 partition of p (constraint check_a check (a > 0) no
inherit) for values in (1);
NOTICE: merging constraint "check_a" with inherited definition
\d p1
Table "public.p1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | not null |
Partition of: p FOR VALUES IN (1)
Check constraints:
"check_a" CHECK (a > 0) NO INHERIT
I thought we were fine with allowing merging of such child
constraints, because leaf partitions can't have children to pass them
onto, so the NO INHERIT clause is essentially pointless.
- if the one being added is not NO INHERIT, then both constraints
would have the same state and so we silently do nothing.
Maybe we should emit some kind of NOTICE message in such cases?
* if a leaf partition has a locally defined not-null marked NO INHERIT
- if we add another NO INHERIT, silently do nothing
- if we add an INHERIT one, throw an error because cannot merge.
So IIUC, there cannot be multiple *named* NOT NULL constraints for the
same column?
If you want, you could leave the not-null constraint case alone and I
can have a look later. It wasn't my intention to burden you with that.
No worries. I want to ensure that the behaviors for NOT NULL and
CHECK constraints are as consistent as possible.
Anyway, for now, I've fixed my patch to only consider CHECK
constraints -- leaf partitions can have inherited CHECK constraints
that are marked NO INHERIT.
--
Thanks, Amit Langote
Attachments:
v3-0001-Allow-inherited-constraints-to-be-marked-NO-INHER.patchapplication/octet-stream; name=v3-0001-Allow-inherited-constraints-to-be-marked-NO-INHER.patchDownload
From ffed8a642aac8d8cd8011c8fdcebcf174277fb00 Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Thu, 14 Nov 2024 17:11:46 +0900
Subject: [PATCH v3] Allow inherited constraints to be marked NO INHERIT in
leaf partitions
For inheritance child tables, inherited constraints can't be marked
NO INHERIT, as that would prevent their propagation to grandchildren.
Since leaf partitions have no children, marking their inherited
constraints as NO INHERIT is harmless.
Note that this change applies only to CHECK constraints.
Reported-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Author: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Alvaro Herrera <alvherre@alvh.no-ip.org>
Discussion: https://postgr.es/m/202411051201.zody6mld7vkw@alvherre.pgsql
---
doc/src/sgml/ref/alter_table.sgml | 3 +--
src/backend/catalog/heap.c | 18 ++++++++++---
src/backend/commands/tablecmds.c | 17 +++++++++---
src/test/regress/expected/alter_table.out | 16 ++++++++++++
src/test/regress/expected/create_table.out | 28 ++++++++++++++++++++
src/test/regress/sql/alter_table.sql | 17 ++++++++++++
src/test/regress/sql/create_table.sql | 30 ++++++++++++++++++++++
7 files changed, 119 insertions(+), 10 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 6098ebed43..24c027600c 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1020,8 +1020,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
target table. The table to be attached must have all the same columns
as the target table and no more; moreover, the column types must also
match. Also, it must have all the <literal>NOT NULL</literal> and
- <literal>CHECK</literal> constraints of the target table, not marked
- <literal>NO INHERIT</literal>. Currently
+ <literal>CHECK</literal> constraints of the target table. Currently
<literal>FOREIGN KEY</literal> constraints are not considered.
<literal>UNIQUE</literal> and <literal>PRIMARY KEY</literal> constraints
from the parent table will be created in the partition, if they don't
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 003af4bf21..5a4073d055 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2707,8 +2707,15 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
errmsg("constraint \"%s\" for relation \"%s\" already exists",
ccname, RelationGetRelationName(rel))));
- /* If the child constraint is "no inherit" then cannot merge */
- if (con->connoinherit)
+ /*
+ * If the child constraint is "no inherit" then cannot merge because
+ * that would prevent lower-level children from inheriting it. That's
+ * not a concern if the child table is a leaf partition which cannot
+ * have child tables, so we allow the merge.
+ */
+ if (con->connoinherit &&
+ (!rel->rd_rel->relispartition ||
+ RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind)))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with non-inherited constraint on relation \"%s\"",
@@ -2717,9 +2724,12 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
/*
* Must not change an existing inherited constraint to "no inherit"
* status. That's because inherited constraints should be able to
- * propagate to lower-level children.
+ * propagate to lower-level children. It's fine to allow doing so for
+ * leaf partitions, as described above.
*/
- if (con->coninhcount > 0 && is_no_inherit)
+ if (con->coninhcount > 0 && is_no_inherit &&
+ (!rel->rd_rel->relispartition ||
+ RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind)))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with inherited constraint on relation \"%s\"",
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ccd9645e7d..2feae4a526 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -377,7 +377,8 @@ static List *MergeCheckConstraint(List *constraints, const char *name, Node *exp
static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispartition);
-static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
+static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel,
+ bool child_is_partition);
static void StoreCatalogInheritance(Oid relationId, List *supers,
bool child_is_partition);
static void StoreCatalogInheritance1(Oid relationId, Oid parentOid,
@@ -16153,7 +16154,7 @@ CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition)
MergeAttributesIntoExisting(child_rel, parent_rel, ispartition);
/* Match up the constraints and bump coninhcount as needed */
- MergeConstraintsIntoExisting(child_rel, parent_rel);
+ MergeConstraintsIntoExisting(child_rel, parent_rel, ispartition);
/*
* OK, it looks valid. Make the catalog entries that show inheritance.
@@ -16358,7 +16359,8 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
* XXX See MergeWithExistingConstraint too if you change this code.
*/
static void
-MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
+MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel,
+ bool child_is_partition)
{
Relation constraintrel;
SysScanDesc parent_scan;
@@ -16455,8 +16457,15 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
/*
* If the child constraint is "no inherit" then cannot merge
+ * because that would prevent lower-level children from inheriting
+ * it. That's not a concern if the child table is a leaf
+ * partition which cannot have child tables, so we allow the
+ * merge, but only for CHECK constraints.
*/
- if (child_con->connoinherit)
+ if (child_con->connoinherit &&
+ (!child_is_partition ||
+ RELKIND_HAS_PARTITIONS(child_rel->rd_rel->relkind) ||
+ child_con->contype == CONSTRAINT_NOTNULL))
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("constraint \"%s\" conflicts with non-inherited constraint on child table \"%s\"",
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 2212c8dbb5..5f1cf3cc2b 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -4282,6 +4282,22 @@ ERROR: every hash partition modulus must be a factor of the next larger modulus
DETAIL: The new modulus 3 is not a factor of 4, the modulus of existing partition "hpart_1".
DROP TABLE fail_part;
--
+-- Prevent marking a constraint as NO INHERIT on a child table when it
+-- inherits a constraint with the same name from its parent, to ensure that
+-- the grandchildren consistently inherit the constraint.
+--
+CREATE TABLE parent_with_check (a int NOT NULL, CONSTRAINT check_a CHECK (a > 0));
+CREATE TABLE child1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE child1_with_noinherit_check INHERIT parent_with_check; -- not ok
+ERROR: column "a" in child table "child1_with_noinherit_check" must be marked NOT NULL
+DROP TABLE parent_with_check, child1_with_noinherit_check;
+-- No need to have that restriction for leaf partitions though, because they
+-- cannot have any children of their own
+CREATE TABLE parted_parent_with_check (a int, CONSTRAINT check_a CHECK (a > 0)) PARTITION BY LIST (a);
+CREATE TABLE part1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE parted_parent_with_check ATTACH PARTITION part1_with_noinherit_check FOR VALUES IN (1); -- ok
+DROP TABLE parted_parent_with_check;
+--
-- DETACH PARTITION
--
-- check that the table is partitioned at all
diff --git a/src/test/regress/expected/create_table.out b/src/test/regress/expected/create_table.out
index 76604705a9..2f01720dc6 100644
--- a/src/test/regress/expected/create_table.out
+++ b/src/test/regress/expected/create_table.out
@@ -257,6 +257,34 @@ CREATE TABLE partitioned (
CONSTRAINT check_a CHECK (a > 0) NO INHERIT
) PARTITION BY RANGE (a);
ERROR: cannot add NO INHERIT constraint to partitioned table "partitioned"
+-- also when creating the partitioned table as partition
+CREATE TABLE partitioned_with_check (
+ a int NOT NULL,
+ CONSTRAINT check_a CHECK (a > 0)
+) PARTITION BY RANGE (a);
+CREATE TABLE partitioned_with_check_part1
+ PARTITION OF partitioned_with_check (
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) FOR VALUES FROM (1) TO (2)
+PARTITION BY LIST (a); -- not ok
+ERROR: constraint "check_a" conflicts with inherited constraint on relation "partitioned_with_check_part1"
+-- fine if the partition is a leaf partition though
+CREATE TABLE partitioned_with_check_part1
+ PARTITION OF partitioned_with_check (
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) FOR VALUES FROM (1) TO (2); -- ok
+NOTICE: merging constraint "check_a" with inherited definition
+-- similar tests for plain inheritance
+CREATE TABLE ct_parent_with_check (
+ a int,
+ CONSTRAINT check_a CHECK (a > 0));
+CREATE TABLE ct_child1_with_noinherit_check (
+ a int,
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) INHERITS (ct_parent_with_check); -- not ok
+NOTICE: merging column "a" with inherited definition
+ERROR: constraint "check_a" conflicts with inherited constraint on relation "ct_child1_with_noinherit_check"
+DROP TABLE partitioned_with_check, ct_parent_with_check;
-- some checks after successful creation of a partitioned table
CREATE FUNCTION plusone(a int) RETURNS INT AS $$ SELECT a+1; $$ LANGUAGE SQL;
CREATE TABLE partitioned (
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index 637e3dac38..d593e210ad 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -2738,6 +2738,23 @@ ALTER TABLE hash_parted ATTACH PARTITION fail_part FOR VALUES WITH (MODULUS 8, R
ALTER TABLE hash_parted ATTACH PARTITION fail_part FOR VALUES WITH (MODULUS 3, REMAINDER 2);
DROP TABLE fail_part;
+--
+-- Prevent marking a constraint as NO INHERIT on a child table when it
+-- inherits a constraint with the same name from its parent, to ensure that
+-- the grandchildren consistently inherit the constraint.
+--
+CREATE TABLE parent_with_check (a int NOT NULL, CONSTRAINT check_a CHECK (a > 0));
+CREATE TABLE child1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE child1_with_noinherit_check INHERIT parent_with_check; -- not ok
+DROP TABLE parent_with_check, child1_with_noinherit_check;
+
+-- No need to have that restriction for leaf partitions though, because they
+-- cannot have any children of their own
+CREATE TABLE parted_parent_with_check (a int, CONSTRAINT check_a CHECK (a > 0)) PARTITION BY LIST (a);
+CREATE TABLE part1_with_noinherit_check (a int, CONSTRAINT check_a CHECK (a > 0) NO INHERIT);
+ALTER TABLE parted_parent_with_check ATTACH PARTITION part1_with_noinherit_check FOR VALUES IN (1); -- ok
+DROP TABLE parted_parent_with_check;
+
--
-- DETACH PARTITION
--
diff --git a/src/test/regress/sql/create_table.sql b/src/test/regress/sql/create_table.sql
index 37a227148e..79dfdab9ab 100644
--- a/src/test/regress/sql/create_table.sql
+++ b/src/test/regress/sql/create_table.sql
@@ -194,6 +194,36 @@ CREATE TABLE partitioned (
CONSTRAINT check_a CHECK (a > 0) NO INHERIT
) PARTITION BY RANGE (a);
+-- also when creating the partitioned table as partition
+CREATE TABLE partitioned_with_check (
+ a int NOT NULL,
+ CONSTRAINT check_a CHECK (a > 0)
+) PARTITION BY RANGE (a);
+
+CREATE TABLE partitioned_with_check_part1
+ PARTITION OF partitioned_with_check (
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) FOR VALUES FROM (1) TO (2)
+PARTITION BY LIST (a); -- not ok
+
+-- fine if the partition is a leaf partition though
+CREATE TABLE partitioned_with_check_part1
+ PARTITION OF partitioned_with_check (
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) FOR VALUES FROM (1) TO (2); -- ok
+
+-- similar tests for plain inheritance
+CREATE TABLE ct_parent_with_check (
+ a int,
+ CONSTRAINT check_a CHECK (a > 0));
+
+CREATE TABLE ct_child1_with_noinherit_check (
+ a int,
+ CONSTRAINT check_a CHECK (a > 0) NO INHERIT
+) INHERITS (ct_parent_with_check); -- not ok
+
+DROP TABLE partitioned_with_check, ct_parent_with_check;
+
-- some checks after successful creation of a partitioned table
CREATE FUNCTION plusone(a int) RETURNS INT AS $$ SELECT a+1; $$ LANGUAGE SQL;
--
2.43.0
On 2024-Nov-14, Amit Langote wrote:
Sorry, here's the full example. Note I'd changed both
AddRelationNotNullConstraints() and AdjustNotNullInheritance() to not
throw an error *if* the table is a leaf partition when the NO INHERIT
of an existing constraint doesn't match that of the new constraint.create table p (a int not null) partition by list (a);
create table p1 partition of p for values in (1);
alter table p1 add constraint a_nn_ni not null a no inherit;
Yeah, I think this behavior is bogus, because the user wants to have
something (a constraint that will not inherit) but they cannot have it,
because there is already a constraint that will inherit. The current
behavior of throwing an error seems correct to me. With your patch,
what this does is mark the constraint as "local" in addition to
inherited, but that'd be wrong, because the constraint the user wanted
is not of the same state.
On Wed, Nov 13, 2024 at 10:49 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
I think:
* if a leaf partition already has an inherited not-null constraint
from its parent and we want to add another one, we should:
- if the one being added is NO INHERIT, then throw an error, because
we cannot merge themHmm, we'll be doing something else for CHECK constraints if we apply my patch:
create table p (a int not null, constraint check_a check (a > 0)) partition by list (a);
create table p1 partition of p (constraint check_a check (a > 0) no inherit) for values in (1);
NOTICE: merging constraint "check_a" with inherited definition\d p1
Table "public.p1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | not null |
Partition of: p FOR VALUES IN (1)
Check constraints:
"check_a" CHECK (a > 0) NO INHERITI thought we were fine with allowing merging of such child
constraints, because leaf partitions can't have children to pass them
onto, so the NO INHERIT clause is essentially pointless.
I'm beginning to have second thoughts about this whole thing TBH, as it
feels inconsistent -- and unnecessary, if we get the patch to flip the
INHERIT/NO INHERIT flag of constraints.
- if the one being added is not NO INHERIT, then both constraints
would have the same state and so we silently do nothing.Maybe we should emit some kind of NOTICE message in such cases?
Hmm, I'm not sure. It's not terribly useful, is it? I mean, if the
user wants to have a constraint, then whether the constraint is there or
not, the end result is the same and we needn't say anything about it.
It's only if the constraint is not what they want (because of
mismatching INHERIT flag) that we throw some message.
(I wonder if we'd throw an error if the proposed constraint has a
different _name_ from the existing constraint. If a DDL script is
expecting that the constraint will be named a certain way, then by
failing to throw an error we might be giving confusing expectations.)
* if a leaf partition has a locally defined not-null marked NO INHERIT
- if we add another NO INHERIT, silently do nothing
- if we add an INHERIT one, throw an error because cannot merge.So IIUC, there cannot be multiple *named* NOT NULL constraints for the
same column?
That's correct. What I mean is that if you have a constraint, and you
try to add another, then the reaction is to compare the desired
constraint with the existing one; if the comparison yields okay, then we
silently do nothing; if the comparison says both things are
incompatible, we throw an error. In no case would we add a second
constraint.
If you want, you could leave the not-null constraint case alone and I
can have a look later. It wasn't my intention to burden you with that.No worries. I want to ensure that the behaviors for NOT NULL and
CHECK constraints are as consistent as possible.
Sounds good.
Anyway, for now, I've fixed my patch to only consider CHECK
constraints -- leaf partitions can have inherited CHECK constraints
that are marked NO INHERIT.
I agree that both types of constraints should behave as similarly as
possible in as many ways as possible. Behavioral differences are
unlikely to be cherished by users.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
On Tue, Nov 19, 2024 at 6:41 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Nov-14, Amit Langote wrote:
Sorry, here's the full example. Note I'd changed both
AddRelationNotNullConstraints() and AdjustNotNullInheritance() to not
throw an error *if* the table is a leaf partition when the NO INHERIT
of an existing constraint doesn't match that of the new constraint.create table p (a int not null) partition by list (a);
create table p1 partition of p for values in (1);
alter table p1 add constraint a_nn_ni not null a no inherit;Yeah, I think this behavior is bogus, because the user wants to have
something (a constraint that will not inherit) but they cannot have it,
because there is already a constraint that will inherit. The current
behavior of throwing an error seems correct to me. With your patch,
what this does is mark the constraint as "local" in addition to
inherited, but that'd be wrong, because the constraint the user wanted
is not of the same state.
Yeah, just not throwing the error, as my patch did, is not enough.
The patch didn't do anything to ensure that a separate constraint with
the properties that the user entered will exist alongside the
inherited one, but that's not possible given that the design only
allows one NOT NULL constraint for a column as you've written below.
On Wed, Nov 13, 2024 at 10:49 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
I think:
* if a leaf partition already has an inherited not-null constraint
from its parent and we want to add another one, we should:
- if the one being added is NO INHERIT, then throw an error, because
we cannot merge themHmm, we'll be doing something else for CHECK constraints if we apply my patch:
create table p (a int not null, constraint check_a check (a > 0)) partition by list (a);
create table p1 partition of p (constraint check_a check (a > 0) no inherit) for values in (1);
NOTICE: merging constraint "check_a" with inherited definition\d p1
Table "public.p1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
a | integer | | not null |
Partition of: p FOR VALUES IN (1)
Check constraints:
"check_a" CHECK (a > 0) NO INHERITI thought we were fine with allowing merging of such child
constraints, because leaf partitions can't have children to pass them
onto, so the NO INHERIT clause is essentially pointless.I'm beginning to have second thoughts about this whole thing TBH, as it
feels inconsistent -- and unnecessary, if we get the patch to flip the
INHERIT/NO INHERIT flag of constraints.
Ah, ok, I haven't looked at that patch, but I am happy to leave this alone.
- if the one being added is not NO INHERIT, then both constraints
would have the same state and so we silently do nothing.Maybe we should emit some kind of NOTICE message in such cases?
Hmm, I'm not sure. It's not terribly useful, is it? I mean, if the
user wants to have a constraint, then whether the constraint is there or
not, the end result is the same and we needn't say anything about it.It's only if the constraint is not what they want (because of
mismatching INHERIT flag) that we throw some message.(I wonder if we'd throw an error if the proposed constraint has a
different _name_ from the existing constraint. If a DDL script is
expecting that the constraint will be named a certain way, then by
failing to throw an error we might be giving confusing expectations.)
Here's an example that I think matches the above description, which,
ISTM, leads to a state similar to what one would encounter with my
patch as described earlier:
create table parent (a int not null);
create table child (a int, constraint a_not_null_child not null a)
inherits (parent);
NOTICE: merging column "a" with inherited definition
\d+ child
Table "public.child"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
a | integer | | not null | | plain |
| |
Not-null constraints:
"a_not_null_child" NOT NULL "a" (local, inherited)
Inherits: parent
Access method: heap
I think the inherited constraint should be named parent_a_not_null,
but the constraint that gets stored is the user-specified constraint
in `create table child`. Actually, even the automatically generated
constraint name using the child table's name won't match the name of
the inherited constraint:
create table child (a int not null) inherits (parent);
NOTICE: merging column "a" with inherited definition
\d+ child
Table "public.child"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
a | integer | | not null | | plain |
| |
Not-null constraints:
"child_a_not_null" NOT NULL "a" (local, inherited)
Inherits: parent
Access method: heap
Not sure, but perhaps the following piece of code of
AddRelationNotNullConstraints() should also check that the names
match?
/*
* Search in the list of inherited constraints for any entries on the
* same column; determine an inheritance count from that. Also, if at
* least one parent has a constraint for this column, then we must not
* accept a user specification for a NO INHERIT one. Any constraint
* from parents that we process here is deleted from the list: we no
* longer need to process it in the loop below.
*/
foreach_ptr(CookedConstraint, old, old_notnulls)
{
if (old->attnum == attnum)
{
/*
* If we get a constraint from the parent, having a local NO
* INHERIT one doesn't work.
*/
if (constr->is_no_inherit)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("cannot define not-null constraint
on column \"%s\" with NO INHERIT",
strVal(linitial(constr->keys))),
errdetail("The column has an inherited
not-null constraint.")));
inhcount++;
old_notnulls = foreach_delete_current(old_notnulls, old);
}
}
Unless the inherited NOT NULL constraints are not required to have the
same name.
* if a leaf partition has a locally defined not-null marked NO INHERIT
- if we add another NO INHERIT, silently do nothing
- if we add an INHERIT one, throw an error because cannot merge.So IIUC, there cannot be multiple *named* NOT NULL constraints for the
same column?That's correct. What I mean is that if you have a constraint, and you
try to add another, then the reaction is to compare the desired
constraint with the existing one; if the comparison yields okay, then we
silently do nothing; if the comparison says both things are
incompatible, we throw an error. In no case would we add a second
constraint.If you want, you could leave the not-null constraint case alone and I
can have a look later. It wasn't my intention to burden you with that.No worries. I want to ensure that the behaviors for NOT NULL and
CHECK constraints are as consistent as possible.Sounds good.
Anyway, for now, I've fixed my patch to only consider CHECK
constraints -- leaf partitions can have inherited CHECK constraints
that are marked NO INHERIT.I agree that both types of constraints should behave as similarly as
possible in as many ways as possible. Behavioral differences are
unlikely to be cherished by users.
To be clear, I suppose we agree on continuing to throw an error when
trying to define a NO INHERIT CHECK constraint on a leaf partition.
That is to match the behavior we currently (as of 14e87ffa5) have for
NOT NULL constraints.
--
Thanks, Amit Langote
On 2024-Nov-20, Amit Langote wrote:
On Tue, Nov 19, 2024 at 6:41 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Here's an example that I think matches the above description, which,
ISTM, leads to a state similar to what one would encounter with my
patch as described earlier:create table parent (a int not null);
create table child (a int, constraint a_not_null_child not null a)
inherits (parent);
NOTICE: merging column "a" with inherited definition\d+ child
Table "public.child"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
a | integer | | not null | | plain |
| |
Not-null constraints:
"a_not_null_child" NOT NULL "a" (local, inherited)
Inherits: parent
Access method: heapI think the inherited constraint should be named parent_a_not_null,
but the constraint that gets stored is the user-specified constraint
in `create table child`.
Yeah, the user-specified name taking precedence over using a name from
inheritance was an explicit decision. I think this is working as
intended. An important point is that pg_dump should preserve any
constraint name; that's for example why we disallow ALTER TABLE DROP
CONSTRAINT of an inherited constraint: previously I made that command
reset the 'islocal' flag of the constraint and otherwise do nothing; but
the problem is that if we allow that, then the constraint gets the wrong
name after dump/restore.
Not sure, but perhaps the following piece of code of
AddRelationNotNullConstraints() should also check that the names
match?/*
* Search in the list of inherited constraints for any entries on the
* same column; determine an inheritance count from that. Also, if at
* least one parent has a constraint for this column, then we must not
* accept a user specification for a NO INHERIT one. Any constraint
* from parents that we process here is deleted from the list: we no
* longer need to process it in the loop below.
*/
I was of two minds about checking that the constraint names match, but
in the end I decided it wasn't useful and limiting, because you cannot
have a particular name in the children if you want to.
One thing that distinguishes not-null constraints from check ones is
that when walking down inheritance trees we match by the column that the
apply to, rather than by name as check constraints do. So the fact that
the names don't match doesn't harm. That would not fly for check
constraints.
Unless the inherited NOT NULL constraints are not required to have the
same name.
Yep.
I agree that both types of constraints should behave as similarly as
possible in as many ways as possible. Behavioral differences are
unlikely to be cherished by users.To be clear, I suppose we agree on continuing to throw an error when
trying to define a NO INHERIT CHECK constraint on a leaf partition.
That is to match the behavior we currently (as of 14e87ffa5) have for
NOT NULL constraints.
Yeah. I think we should only worry to the extent that these things
trouble users over what they can and cannot do with tables. Adding a NO
INHERIT constraint to a partition, just so that they can continue to
have the constraint after they detach and that the constraint doesn't
affect any tables that are added as children ... doesn't seem a very
important use case. _But_, for anybody out there that does care about
such a thing, we might have an ALTER TABLE command to change a
constraint from NO INHERIT to INHERIT, and perhaps vice-versa.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"No es bueno caminar con un hombre muerto"
On Wed, Nov 27, 2024 at 12:53 AM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2024-Nov-20, Amit Langote wrote:
On Tue, Nov 19, 2024 at 6:41 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Here's an example that I think matches the above description, which,
ISTM, leads to a state similar to what one would encounter with my
patch as described earlier:create table parent (a int not null);
create table child (a int, constraint a_not_null_child not null a)
inherits (parent);
NOTICE: merging column "a" with inherited definition\d+ child
Table "public.child"
Column | Type | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
a | integer | | not null | | plain |
| |
Not-null constraints:
"a_not_null_child" NOT NULL "a" (local, inherited)
Inherits: parent
Access method: heapI think the inherited constraint should be named parent_a_not_null,
but the constraint that gets stored is the user-specified constraint
in `create table child`.Yeah, the user-specified name taking precedence over using a name from
inheritance was an explicit decision. I think this is working as
intended. An important point is that pg_dump should preserve any
constraint name; that's for example why we disallow ALTER TABLE DROP
CONSTRAINT of an inherited constraint: previously I made that command
reset the 'islocal' flag of the constraint and otherwise do nothing; but
the problem is that if we allow that, then the constraint gets the wrong
name after dump/restore.
Ok, I see. I didn't think about the pg_dump / restore aspect of this.
Not sure, but perhaps the following piece of code of
AddRelationNotNullConstraints() should also check that the names
match?/*
* Search in the list of inherited constraints for any entries on the
* same column; determine an inheritance count from that. Also, if at
* least one parent has a constraint for this column, then we must not
* accept a user specification for a NO INHERIT one. Any constraint
* from parents that we process here is deleted from the list: we no
* longer need to process it in the loop below.
*/I was of two minds about checking that the constraint names match, but
in the end I decided it wasn't useful and limiting, because you cannot
have a particular name in the children if you want to.One thing that distinguishes not-null constraints from check ones is
that when walking down inheritance trees we match by the column that the
apply to, rather than by name as check constraints do. So the fact that
the names don't match doesn't harm. That would not fly for check
constraints.
Yeah, that makes sense.
Unless the inherited NOT NULL constraints are not required to have the
same name.Yep.
I agree that both types of constraints should behave as similarly as
possible in as many ways as possible. Behavioral differences are
unlikely to be cherished by users.To be clear, I suppose we agree on continuing to throw an error when
trying to define a NO INHERIT CHECK constraint on a leaf partition.
That is to match the behavior we currently (as of 14e87ffa5) have for
NOT NULL constraints.Yeah. I think we should only worry to the extent that these things
trouble users over what they can and cannot do with tables. Adding a NO
INHERIT constraint to a partition, just so that they can continue to
have the constraint after they detach and that the constraint doesn't
affect any tables that are added as children ... doesn't seem a very
important use case. _But_, for anybody out there that does care about
such a thing, we might have an ALTER TABLE command to change a
constraint from NO INHERIT to INHERIT, and perhaps vice-versa.
+1
--
Thanks, Amit Langote