Disallow direct change of NO INHERIT of not-null constraints

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
This commit is contained in:
Alvaro Herrera 2024-05-02 17:26:30 +02:00
parent 42510c031b
commit d45597f72f
No known key found for this signature in database
GPG Key ID: 1C20ACB9D5C564AE
7 changed files with 92 additions and 14 deletions

View 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> |

View 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)

View 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 */
}

View 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

View 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);

View 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
--

View 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
--