Disallow direct change of NO INHERIT of not-null constraints
authorAlvaro Herrera <alvherre@alvh.no-ip.org>
Thu, 2 May 2024 15:26:30 +0000 (17:26 +0200)
committerAlvaro Herrera <alvherre@alvh.no-ip.org>
Thu, 2 May 2024 15:26:30 +0000 (17:26 +0200)
We support changing NO INHERIT constraint to INHERIT for constraints in
child relations when adding a constraint to some ancestor relation, and
also during pg_upgrade's schema restore; but other than those special
cases, command ALTER TABLE ADD CONSTRAINT should not be allowed to
change an existing constraint from NO INHERIT to INHERIT, as that would
require to process child relations so that they also acquire an
appropriate constraint, which we may not be in a position to do.  (It'd
also be surprising behavior.)

It is conceivable that we want to allow ALTER TABLE SET NOT NULL to make
such a change; but in that case some more code is needed to implement it
correctly, so for now I've made that throw the same error message.

Also, during the prep phase of ALTER TABLE ADD CONSTRAINT, acquire locks
on all descendant tables; otherwise we might operate on child tables on
which no locks are held, particularly in the mode where a primary key
causes not-null constraints to be created on children.

Reported-by: Alexander Lakhin <exclusion@gmail.com>
Discussion: https://postgr.es/m/7d923a66-55f0-3395-cd40-81c142b5448b@gmail.com

doc/src/sgml/ref/alter_table.sgml
src/backend/catalog/heap.c
src/backend/catalog/pg_constraint.c
src/backend/commands/tablecmds.c
src/include/catalog/pg_constraint.h
src/test/regress/expected/inherit.out
src/test/regress/sql/inherit.sql

index ebd8c62038ce755f986b2b3441ff7bb66019ae40..0bf11f6cb6d8d7963bf286361452be04e990946d 100644 (file)
@@ -105,7 +105,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 <phrase>and <replaceable class="parameter">column_constraint</replaceable> is:</phrase>
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
-{ NOT NULL |
+{ NOT NULL [ NO INHERIT ] |
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   DEFAULT <replaceable>default_expr</replaceable> |
index f0278b9c0175d35b3fa4d762ef939d9b35e7958e..136cc42a92f07ed779df23b11893e77bb2fe977a 100644 (file)
@@ -2549,13 +2549,23 @@ AddRelationNewConstraints(Relation rel,
 
            /*
             * If the column already has an inheritable not-null constraint,
-            * we need only adjust its inheritance status and we're done.  If
-            * the constraint is there but marked NO INHERIT, then it is
-            * updated in the same way, but we also recurse to the children
-            * (if any) to add the constraint there as well.
+            * we need only adjust its coninhcount and we're done.  In certain
+            * cases (see below), if the constraint is there but marked NO
+            * INHERIT, then we mark it as no longer such and coninhcount
+            * updated, plus we must also recurse to the children (if any) to
+            * add the constraint there.
+            *
+            * We only allow the inheritability status to change during binary
+            * upgrade (where it's used to add the not-null constraints for
+            * children of tables with primary keys), or when we're recursing
+            * processing a table down an inheritance hierarchy; directly
+            * allowing a constraint to change from NO INHERIT to INHERIT
+            * during ALTER TABLE ADD CONSTRAINT would be far too surprising
+            * behavior.
             */
            existing = AdjustNotNullInheritance1(RelationGetRelid(rel), colnum,
-                                                cdef->inhcount, cdef->is_no_inherit);
+                                                cdef->inhcount, cdef->is_no_inherit,
+                                                IsBinaryUpgrade || allow_merge);
            if (existing == 1)
                continue;       /* all done! */
            else if (existing == -1)
index aaf3537d3f507e2b1c5513328db28acbae39e0fc..6b8496e0850ff0a7591f48256551c5e0ae07ca7e 100644 (file)
@@ -721,7 +721,7 @@ extractNotNullColumn(HeapTuple constrTup)
  */
 int
 AdjustNotNullInheritance1(Oid relid, AttrNumber attnum, int count,
-                         bool is_no_inherit)
+                         bool is_no_inherit, bool allow_noinherit_change)
 {
    HeapTuple   tup;
 
@@ -744,16 +744,23 @@ AdjustNotNullInheritance1(Oid relid, AttrNumber attnum, int count,
        if (is_no_inherit && !conform->connoinherit)
            ereport(ERROR,
                    errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-                   errmsg("cannot change NO INHERIT status of inherited NOT NULL constraint \"%s\" on relation \"%s\"",
+                   errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
                           NameStr(conform->conname), get_rel_name(relid)));
 
        /*
         * If the constraint already exists in this relation but it's marked
-        * NO INHERIT, we can just remove that flag, and instruct caller to
-        * recurse to add the constraint to children.
+        * NO INHERIT, we can just remove that flag (provided caller allows
+        * such a change), and instruct caller to recurse to add the
+        * constraint to children.
         */
        if (!is_no_inherit && conform->connoinherit)
        {
+           if (!allow_noinherit_change)
+               ereport(ERROR,
+                       errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                       errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" in relation \"%s\"",
+                              NameStr(conform->conname), get_rel_name(relid)));
+
            conform->connoinherit = false;
            retval = -1;        /* caller must add constraint on child rels */
        }
index 08c87e60293a7e5aa780986f47ec3340324b95b2..c31783373f0f4b408adc04731dd38d7fe1161eba 100644 (file)
@@ -4980,10 +4980,12 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
            break;
        case AT_AddConstraint:  /* ADD CONSTRAINT */
            ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
-           /* Recursion occurs during execution phase */
-           /* No command-specific prep needed except saving recurse flag */
            if (recurse)
+           {
+               /* recurses at exec time; lock descendants and set flag */
+               (void) find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
                cmd->recurse = true;
+           }
            pass = AT_PASS_ADD_CONSTR;
            break;
        case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
@@ -7892,6 +7894,17 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
        copytup = heap_copytuple(tuple);
        conForm = (Form_pg_constraint) GETSTRUCT(copytup);
 
+       /*
+        * Don't let a NO INHERIT constraint be changed into inherit.
+        */
+       if (conForm->connoinherit && recurse)
+           ereport(ERROR,
+                   errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                   errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" in relation \"%s\"",
+                          NameStr(conForm->conname),
+                          RelationGetRelationName(rel)));
+
+
        /*
         * If we find an appropriate constraint, we're almost done, but just
         * need to change some properties on it: if we're recursing, increment
index 5c52d71e091a87546a007376c41e30036a0ecae7..68bf55fdf7024ca33bb911a0db99954ea67dc9a4 100644 (file)
@@ -262,7 +262,7 @@ extern HeapTuple findNotNullConstraint(Oid relid, const char *colname);
 extern HeapTuple findDomainNotNullConstraint(Oid typid);
 extern AttrNumber extractNotNullColumn(HeapTuple constrTup);
 extern int AdjustNotNullInheritance1(Oid relid, AttrNumber attnum, int count,
-                                     bool is_no_inherit);
+                                     bool is_no_inherit, bool allow_noinherit_change);
 extern void AdjustNotNullInheritance(Oid relid, Bitmapset *columns, int count);
 extern List *RelationGetNotNullConstraints(Oid relid, bool cooked);
 
index 5a5c23fc3b018296fc184160d4d6941cfb1b5751..eaa65049c7b1b18724bb5587c0fa42275091e951 100644 (file)
@@ -2126,7 +2126,7 @@ ERROR:  cannot define not-null constraint on column "a2" with NO INHERIT
 DETAIL:  The column has an inherited not-null constraint.
 -- change NO INHERIT status of inherited constraint: no dice, it's inherited
 alter table cc2 add not null a2 no inherit;
-ERROR:  cannot change NO INHERIT status of inherited NOT NULL constraint "nn" on relation "cc2"
+ERROR:  cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "cc2"
 -- remove constraint from cc2: no dice, it's inherited
 alter table cc2 alter column a2 drop not null;
 ERROR:  cannot drop inherited constraint "nn" of relation "cc2"
@@ -2277,6 +2277,34 @@ Child tables: inh_nn_child,
               inh_nn_child2
 
 drop table inh_nn_parent, inh_nn_child, inh_nn_child2;
+CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT);
+CREATE TABLE inh_nn_child() INHERITS (inh_nn_parent);
+ALTER TABLE inh_nn_parent ADD CONSTRAINT nna NOT NULL a;
+ERROR:  cannot change NO INHERIT status of NOT NULL constraint "inh_nn_parent_a_not_null" in relation "inh_nn_parent"
+ALTER TABLE inh_nn_parent ALTER a SET NOT NULL;
+ERROR:  cannot change NO INHERIT status of NOT NULL constraint "inh_nn_parent_a_not_null" in relation "inh_nn_parent"
+DROP TABLE inh_nn_parent cascade;
+NOTICE:  drop cascades to table inh_nn_child
+-- Adding a PK at the top level of a hierarchy should cause all descendants
+-- to be checked for nulls, even past a no-inherit constraint
+CREATE TABLE inh_nn_lvl1 (a int);
+CREATE TABLE inh_nn_lvl2 () INHERITS (inh_nn_lvl1);
+CREATE TABLE inh_nn_lvl3 (CONSTRAINT foo NOT NULL a NO INHERIT) INHERITS (inh_nn_lvl2);
+CREATE TABLE inh_nn_lvl4 () INHERITS (inh_nn_lvl3);
+CREATE TABLE inh_nn_lvl5 () INHERITS (inh_nn_lvl4);
+INSERT INTO inh_nn_lvl2 VALUES (NULL);
+ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a);
+ERROR:  column "a" of relation "inh_nn_lvl2" contains null values
+DELETE FROM inh_nn_lvl2;
+INSERT INTO inh_nn_lvl5 VALUES (NULL);
+ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a);
+ERROR:  column "a" of relation "inh_nn_lvl5" contains null values
+DROP TABLE inh_nn_lvl1 CASCADE;
+NOTICE:  drop cascades to 4 other objects
+DETAIL:  drop cascades to table inh_nn_lvl2
+drop cascades to table inh_nn_lvl3
+drop cascades to table inh_nn_lvl4
+drop cascades to table inh_nn_lvl5
 --
 -- test inherit/deinherit
 --
index 277fb74d2c0566b488e170df9f857ed360b472ea..2205e59affa4cd509a0a470c567dae019f2ca0e2 100644 (file)
@@ -844,6 +844,26 @@ select conrelid::regclass, conname, contype, conkey,
 \d+ inh_nn*
 drop table inh_nn_parent, inh_nn_child, inh_nn_child2;
 
+CREATE TABLE inh_nn_parent (a int, NOT NULL a NO INHERIT);
+CREATE TABLE inh_nn_child() INHERITS (inh_nn_parent);
+ALTER TABLE inh_nn_parent ADD CONSTRAINT nna NOT NULL a;
+ALTER TABLE inh_nn_parent ALTER a SET NOT NULL;
+DROP TABLE inh_nn_parent cascade;
+
+-- Adding a PK at the top level of a hierarchy should cause all descendants
+-- to be checked for nulls, even past a no-inherit constraint
+CREATE TABLE inh_nn_lvl1 (a int);
+CREATE TABLE inh_nn_lvl2 () INHERITS (inh_nn_lvl1);
+CREATE TABLE inh_nn_lvl3 (CONSTRAINT foo NOT NULL a NO INHERIT) INHERITS (inh_nn_lvl2);
+CREATE TABLE inh_nn_lvl4 () INHERITS (inh_nn_lvl3);
+CREATE TABLE inh_nn_lvl5 () INHERITS (inh_nn_lvl4);
+INSERT INTO inh_nn_lvl2 VALUES (NULL);
+ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a);
+DELETE FROM inh_nn_lvl2;
+INSERT INTO inh_nn_lvl5 VALUES (NULL);
+ALTER TABLE inh_nn_lvl1 ADD PRIMARY KEY (a);
+DROP TABLE inh_nn_lvl1 CASCADE;
+
 --
 -- test inherit/deinherit
 --