Обсуждение: NOT NULL NOT ENFORCED

Поиск
Список
Период
Сортировка

NOT NULL NOT ENFORCED

От
jian he
Дата:
hi.

attached patch will remove sql_features.txt item F492: "Optional table
constraint
enforcement" Comment: "except not-null constraints". see [1]

main points about NOT NULL NOT ENFORCED
* one column can have at most one NOT-NULL constraint, regardless constraints
  property (not enforced or enforced)
* if column already have not enforced not-null constraint then:
  ALTER TABLE ALTER COLUMN SET NOT NULL: error out, can not validate
not enforced not-null constraint
  ALTER TABLE ADD NOT NULL: error out, can not add another not-null
constraint, one column can only have one.

not null in partitioned table:
* If the partitioned table has an enforced not-null constraint, its partitions
cannot have not enforced.
* If the partitioned table has a NOT ENFORCED not-null constraint, its
partitions may have either ENFORCED or NOT ENFORCED not-null constraints, but
the constraint itself is still required.

not null in table inheritance:
OK: parent is not enforced, while child is enforced
NOT OK: parent is enforced, while child is not enforced
If a column inherits from multiple tables and the ancestor tables have
conflicting ENFORCED statuses, raise an error.

I have written extensive tests to cover the corner case, the tests may be
overwhelming.

[1]: https://www.postgresql.org/docs/devel/features-sql-standard.html

Вложения

Re: NOT NULL NOT ENFORCED

От
Álvaro Herrera
Дата:
On 2025-Sep-04, jian he wrote:

> @@ -3093,6 +3115,16 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
>                      conname = other->name;
>  
>                  inhcount++;
> +
> +                /*
> +                 * if a column inherit multiple not-null constraints, the
> +                 * enforced status should the same.
> +                */
> +                if (other->is_enforced != cooked->is_enforced)
> +                    ereport(ERROR,
> +                            errcode(ERRCODE_DATATYPE_MISMATCH),
> +                            errmsg("cannot define not-null constraint on column \"%s\"", conname),
> +                            errdetail("The column inherited not-null constraints have conflict ENFORCED status."));
>                  old_notnulls = list_delete_nth_cell(old_notnulls, restpos);
>              }
>              else

Hmmm, are you sure about this?   I think if a table has two parents, one
with enforced and the other with not enforced constraint, then it's okay
to get them combined resulting in one enforced constraint.

> @@ -777,6 +778,18 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
>                      errhint("You might need to validate it using %s.",
>                              "ALTER TABLE ... VALIDATE CONSTRAINT"));
>  
> +        /*
> +         * If the ENFORCED status we're asked for doesn't match what the
> +         * existing constraint has, throw an error.
> +         */
> +        if (is_enforced != conform->conenforced)
> +            ereport(ERROR,
> +                    errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
> +                    errmsg("cannot change ENFORCED status of NOT NULL constraint \"%s\" on relation \"%s\"",
> +                           NameStr(conform->conname), get_rel_name(relid)),
> +                    errhint("You might need to drop the existing not enforced constraint using %s.",
> +                            "ALTER TABLE ... DROP CONSTRAINT"));

I think the hint here should suggest to make the existing constraint as
enforced, rather than drop it.

> @@ -9937,9 +9962,9 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
>           * If adding a valid not-null constraint, set the pg_attribute flag
>           * and tell phase 3 to verify existing rows, if needed.  For an
>           * invalid constraint, just set attnotnull, without queueing
> -         * verification.
> +         * verification. For not enforced not-null, no need set attnotnull.
>           */
> -        if (constr->contype == CONSTR_NOTNULL)
> +        if (constr->contype == CONSTR_NOTNULL && ccon->is_enforced)
>              set_attnotnull(wqueue, rel, ccon->attnum,
>                             !constr->skip_validation,
>                             !constr->skip_validation);

Didn't we decide that attnotnull meant whether a constraint exists at
all, without making a judgement on whether it's enforced or valid?  The
important change should be in CheckNNConstraintFetch() which should
determine attnullability in a way that allows executor know whether the
column is nullable or not.  I admit we might want to handle this
differently for unenforced constraints, but we should discuss that and
make sure that's what we want.

> +        else if (notenforced)
> +        {
> +            /*
> +             * We can't use ATExecSetNotNull here because it adds an enforced
> +             * not-null constraint, but here we only want a non-enforced one.
> +            */

Umm, wouldn't it make more sense to modify ATExecSetNotNull() so that it
does what we want?  This seems hackish.

> @@ -1272,33 +1294,41 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
>       * Reproduce not-null constraints, if any, by copying them.  We do this
>       * regardless of options given.
>       */
> -    if (tupleDesc->constr && tupleDesc->constr->has_not_null)
> -    {
> -        List       *lst;
> +    lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
> +                                        true);
> +    cxt->nnconstraints = list_concat(cxt->nnconstraints, lst);
>  
> -        lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
> -                                            true);

> +    /*
> +     * When creating a new relation, marking the enforced not-null constraint as
> +     * not valid doesn't make sense, so we treat it as valid.
> +    */
> +    foreach_node(Constraint, nnconstr, lst)
> +    {
> +        if (nnconstr->is_enforced)
> +        {
> +            nnconstr->skip_validation = false;
> +            nnconstr->initially_valid = true;
> +        }
> +    }

Hmmm, this bit here (making constraints as valid if they're not valid in
the source table) looks like a fix for the existing code.  I think it
should be a separate patch, perhaps back-patchable to 18.  Or maybe I'm
missing something ...?

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/
"Digital and video cameras have this adjustment and film cameras don't for the
same reason dogs and cats lick themselves: because they can."   (Ken Rockwell)



Re: NOT NULL NOT ENFORCED

От
jian he
Дата:
On Thu, Sep 4, 2025 at 8:00 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
>
> > @@ -1272,33 +1294,41 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
> >        * Reproduce not-null constraints, if any, by copying them.  We do this
> >        * regardless of options given.
> >        */
> > -     if (tupleDesc->constr && tupleDesc->constr->has_not_null)
> > -     {
> > -             List       *lst;
> > +     lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
> > +                                                                             true);
> > +     cxt->nnconstraints = list_concat(cxt->nnconstraints, lst);
> >
> > -             lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
> > -                                                                                     true);
>
> > +     /*
> > +      * When creating a new relation, marking the enforced not-null constraint as
> > +      * not valid doesn't make sense, so we treat it as valid.
> > +     */
> > +     foreach_node(Constraint, nnconstr, lst)
> > +     {
> > +             if (nnconstr->is_enforced)
> > +             {
> > +                     nnconstr->skip_validation = false;
> > +                     nnconstr->initially_valid = true;
> > +             }
> > +     }
>
> Hmmm, this bit here (making constraints as valid if they're not valid in
> the source table) looks like a fix for the existing code.  I think it
> should be a separate patch, perhaps back-patchable to 18.  Or maybe I'm
> missing something ...?
>

it's indeed a bug, which was introduced

https://git.postgresql.org/cgit/postgresql.git/diff/src/backend/parser/parse_utilcmd.c?id=ca87c415e2fccf81cec6fd45698dde9fae0ab570

attached is the fix, also added a test on create_table_like.sql

Вложения

Re: NOT NULL NOT ENFORCED

От
Álvaro Herrera
Дата:
On 2025-Sep-04, Álvaro Herrera wrote:

> On 2025-Sep-04, jian he wrote:

> > +        else if (notenforced)
> > +        {
> > +            /*
> > +             * We can't use ATExecSetNotNull here because it adds an enforced
> > +             * not-null constraint, but here we only want a non-enforced one.
> > +            */
> 
> Umm, wouldn't it make more sense to modify ATExecSetNotNull() so that it
> does what we want?  This seems hackish.

BTW while you're at that, it might make sense to allow commands like
  ALTER TABLE foo ALTER col1 SET NOT NULL NOT VALID
  ALTER TABLE foo ALTER col1 SET NOT NULL NOT ENFORCED

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"La verdad no siempre es bonita, pero el hambre de ella sí"



Re: NOT NULL NOT ENFORCED

От
jian he
Дата:
On Thu, Sep 4, 2025 at 8:00 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
>
> > @@ -9937,9 +9962,9 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
> >                * If adding a valid not-null constraint, set the pg_attribute flag
> >                * and tell phase 3 to verify existing rows, if needed.  For an
> >                * invalid constraint, just set attnotnull, without queueing
> > -              * verification.
> > +              * verification. For not enforced not-null, no need set attnotnull.
> >                */
> > -             if (constr->contype == CONSTR_NOTNULL)
> > +             if (constr->contype == CONSTR_NOTNULL && ccon->is_enforced)
> >                       set_attnotnull(wqueue, rel, ccon->attnum,
> >                                                  !constr->skip_validation,
> >                                                  !constr->skip_validation);
>
> Didn't we decide that attnotnull meant whether a constraint exists at
> all, without making a judgement on whether it's enforced or valid?  The
> important change should be in CheckNNConstraintFetch() which should
> determine attnullability in a way that allows executor know whether the
> column is nullable or not.  I admit we might want to handle this
> differently for unenforced constraints, but we should discuss that and
> make sure that's what we want.
>

In CheckNNConstraintFetch, I changed it to
"""
        if (conform->contype == CONSTRAINT_NOTNULL)
        {
            if (!conform->convalidated && conform->conenforced)
            {
                AttrNumber    attnum;
                attnum = extractNotNullColumn(htup);
                Assert(relation->rd_att->compact_attrs[attnum -
1].attnullability ==
                       ATTNULLABLE_UNKNOWN);
                relation->rd_att->compact_attrs[attnum - 1].attnullability =
                    ATTNULLABLE_INVALID;
            }
            continue;
        }
"""

set pg_attribute.attnotnull to true for not-valid not-null is still
useful for INSERT/UPDATE.
set pg_attribute.attnotnull to true for not-enforced not-null
constraints doesn't have real benefits, IMHO.

If we let pg_attribute.attnotnull to true for not-enforced not-null,
then do we need to change the definition of
TupleConstr->has_not_null?



Re: NOT NULL NOT ENFORCED

От
Álvaro Herrera
Дата:
On 2025-Sep-08, jian he wrote:

> set pg_attribute.attnotnull to true for not-valid not-null is still
> useful for INSERT/UPDATE.
> set pg_attribute.attnotnull to true for not-enforced not-null
> constraints doesn't have real benefits, IMHO.

Yeah, you might be right about this actually.  What we wanted attnotnull
to be set for, as I recall, was so that generic apps could alert the
user that an insert that tries to put a NULL value in that column would
not work, without having to actually execute it.  With a non-enforced


-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/
"The important things in the world are problems with society that we don't
understand at all. The machines will become more complicated but they won't
be more complicated than the societies that run them."    (Freeman Dyson)



Re: NOT NULL NOT ENFORCED

От
Peter Eisentraut
Дата:
On 04.09.25 17:20, jian he wrote:
> On Thu, Sep 4, 2025 at 8:00 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
>>
>>> @@ -1272,33 +1294,41 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
>>>         * Reproduce not-null constraints, if any, by copying them.  We do this
>>>         * regardless of options given.
>>>         */
>>> -     if (tupleDesc->constr && tupleDesc->constr->has_not_null)
>>> -     {
>>> -             List       *lst;
>>> +     lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
>>> +                                                                             true);
>>> +     cxt->nnconstraints = list_concat(cxt->nnconstraints, lst);
>>>
>>> -             lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
>>> -                                                                                     true);
>>
>>> +     /*
>>> +      * When creating a new relation, marking the enforced not-null constraint as
>>> +      * not valid doesn't make sense, so we treat it as valid.
>>> +     */
>>> +     foreach_node(Constraint, nnconstr, lst)
>>> +     {
>>> +             if (nnconstr->is_enforced)
>>> +             {
>>> +                     nnconstr->skip_validation = false;
>>> +                     nnconstr->initially_valid = true;
>>> +             }
>>> +     }
>>
>> Hmmm, this bit here (making constraints as valid if they're not valid in
>> the source table) looks like a fix for the existing code.  I think it
>> should be a separate patch, perhaps back-patchable to 18.  Or maybe I'm
>> missing something ...?
>>
> 
> it's indeed a bug, which was introduced
>
https://git.postgresql.org/cgit/postgresql.git/diff/src/backend/parser/parse_utilcmd.c?id=ca87c415e2fccf81cec6fd45698dde9fae0ab570
> 
> attached is the fix, also added a test on create_table_like.sql

I have committed this fix.




Re: NOT NULL NOT ENFORCED

От
jian he
Дата:
On Thu, Sep 4, 2025 at 8:00 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
>
> On 2025-Sep-04, jian he wrote:
>
> > @@ -3093,6 +3115,16 @@ AddRelationNotNullConstraints(Relation rel, List *constraints,
> >                                       conname = other->name;
> >
> >                               inhcount++;
> > +
> > +                             /*
> > +                              * if a column inherit multiple not-null constraints, the
> > +                              * enforced status should the same.
> > +                             */
> > +                             if (other->is_enforced != cooked->is_enforced)
> > +                                     ereport(ERROR,
> > +                                                     errcode(ERRCODE_DATATYPE_MISMATCH),
> > +                                                     errmsg("cannot define not-null constraint on column \"%s\"",
conname),
> > +                                                     errdetail("The column inherited not-null constraints have
conflictENFORCED status.")); 
> >                               old_notnulls = list_delete_nth_cell(old_notnulls, restpos);
> >                       }
> >                       else
>
> Hmmm, are you sure about this?   I think if a table has two parents, one
> with enforced and the other with not enforced constraint, then it's okay
> to get them combined resulting in one enforced constraint.
>

changed accordingly.
When a column can inherit multiple not-null constraints. If one is not enforced,
another one is enforced then we will install an enforced one.



> > @@ -777,6 +778,18 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
> >                                       errhint("You might need to validate it using %s.",
> >                                                       "ALTER TABLE ... VALIDATE CONSTRAINT"));
> >
> > +             /*
> > +              * If the ENFORCED status we're asked for doesn't match what the
> > +              * existing constraint has, throw an error.
> > +              */
> > +             if (is_enforced != conform->conenforced)
> > +                     ereport(ERROR,
> > +                                     errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
> > +                                     errmsg("cannot change ENFORCED status of NOT NULL constraint \"%s\" on
relation\"%s\"", 
> > +                                                NameStr(conform->conname), get_rel_name(relid)),
> > +                                     errhint("You might need to drop the existing not enforced constraint using
%s.",
> > +                                                     "ALTER TABLE ... DROP CONSTRAINT"));
>
> I think the hint here should suggest to make the existing constraint as
> enforced, rather than drop it.
>

The hint also changed.

+ /*
+ * If the ENFORCED status we're asked for doesn't match what the
+ * existing constraint has, throw an error.
+ */
+ if (is_enforced != conform->conenforced)
+ {
+    if (is_enforced)
+          ereport(ERROR,
+          errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+          errmsg("cannot change not enforced NOT NULL constraint
\"%s\" on relation \"%s\" to enforced",
+                      NameStr(conform->conname), get_rel_name(relid)),
+          errhint("You might need to ensure the existing constraint
is enforced."));
+ else
+           ereport(ERROR,
+           errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+           errmsg("cannot change enforced NOT NULL constraint \"%s\"
on relation \"%s\" to not enforced",
+                       NameStr(conform->conname), get_rel_name(relid)),
+           errhint("You might need to ensure the existing constraint
is not enforced."));
+ }



>
> > +             else if (notenforced)
> > +             {
> > +                     /*
> > +                      * We can't use ATExecSetNotNull here because it adds an enforced
> > +                      * not-null constraint, but here we only want a non-enforced one.
> > +                     */
>
> Umm, wouldn't it make more sense to modify ATExecSetNotNull() so that it
> does what we want?  This seems hackish.
>

modified ATExecSetNotNull for ATExecAlterConstrInheritability usage.
now ATExecSetNotNull is

ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
                               bool recurse, bool recursing, bool
is_enforced, LOCKMODE lockmode)

new patch attached with the pg_dump TAP tests.
currently NOT VALID NOT NULL dumped
constraint separately, NOT NULL NOT ENFORCED constraints can also be dumped
separately.

CREATE TABLE tx3 (x int not null not enforced);

can be dumped as:

CREATE TABLE public.tx3 (x integer);
ALTER TABLE public.tx3 ADD CONSTRAINT tx3_x_not_null NOT NULL x NOT ENFORCED;
---------------
note: currently not enforced check constraint is dumped separately.
CREATE TABLE tx2 (x int check (x > 1) not enforced);
will be dumped as

CREATE TABLE public.tx2 (x integer);
ALTER TABLE public.tx2
    ADD CONSTRAINT tx2_x_check CHECK ((x > 1)) NOT ENFORCED;

Вложения

Re: NOT NULL NOT ENFORCED

От
Álvaro Herrera
Дата:
On 2025-Sep-24, jian he wrote:

> currently NOT VALID NOT NULL dumped
> constraint separately, NOT NULL NOT ENFORCED constraints can also be dumped
> separately.
> 
> CREATE TABLE tx3 (x int not null not enforced);
> 
> can be dumped as:
> 
> CREATE TABLE public.tx3 (x integer);
> ALTER TABLE public.tx3 ADD CONSTRAINT tx3_x_not_null NOT NULL x NOT ENFORCED;
> ---------------
> note: currently not enforced check constraint is dumped separately.

Hmm, I wonder what's the reason for this.  Seems quite useless.  Why
wouldn't we dump unenforced constraint together with the table?  The
case is different from invalid constraints, which have to be created
after data is loaded.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/