Thread: Re: not null constraints, again

Re: not null constraints, again

From
jian he
Date:
On Fri, Oct 4, 2024 at 9:11 PM Alvaro Herrera <alvherre@alvh.no-ip.org> wrote:
>
> Here's v8 of this patch.


in AdjustNotNullInheritance
        if (count > 0)
        {
            conform->coninhcount += count;
            changed = true;
        }
        if (is_local)
        {
            conform->conislocal = true;
            changed = true;
        }

change to

        if (count > 0)
        {
            conform->coninhcount += count;
            changed = true;
        }
        if (is_local && !conform->conislocal)
        {
            conform->conislocal = true;
            changed = true;
        }

then we can save some cycles.

-------------------<<>>>>------------
MergeConstraintsIntoExisting
            /*
             * If the CHECK 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))));
the comments apply to not-null constraint aslo, so the comments need
to be refactored.

-------------------<<>>>>------------
in ATExecSetNotNull
        if (recursing)
        {
            conForm->coninhcount++;
            changed = true;
        }

grep "coninhcount++", I found out pattern:
        constrForm->coninhcount++;
        if (constrForm->coninhcount < 0)
            ereport(ERROR,
                    errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
                    errmsg("too many inheritance parents"));

here, maybe we can change to
        if (recursing)
        {
            // conForm->coninhcount++;
            if (pg_add_s16_overflow(conForm->coninhcount,1,
&conForm->coninhcount))
                ereport(ERROR,
                        errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
                        errmsg("too many inheritance parents"));
            changed = true;
        }
-------------------<<>>>>------------
base on your reply at [1]

      By contrast, a <literal>NOT NULL</literal> constraint that was created
      as <literal>NO INHERIT</literal> will be changed to a normal inheriting
      one during attach.

these text should removed from section:
<<ATTACH PARTITION partition_name { FOR VALUES partition_bound_spec |
DEFAULT }>>
since currently v8, partition_name not-null no inherit constraint
cannot merge with the parent.

[1] https://www.postgresql.org/message-id/202410021219.bvjmxzdspif2%40alvherre.pgsql



Re: not null constraints, again

From
jian he
Date:
tricky case:
drop table if exists part, part0 cascade;
create table part (a int not null) partition by range (a);
create table part0 (a int primary key);
alter table part attach partition part0 for values from (0) to (1000);
alter table ONLY part add primary key(a);
alter table ONLY part drop constraint part_a_not_null;
-- alter table ONLY part alter column a drop not null;


Now we are in a state where a partitioned
table have a primary key but doesn't have a not-null constraint for it.

select  indisunique, indisprimary, indimmediate,indisvalid
from    pg_index
where   indexrelid = 'part_pkey'::regclass;

shows this primary key index is invalid.

but
select conname,contype,convalidated
from pg_constraint where conname = 'part_pkey';

shows this primary key constraint is valid.



Re: not null constraints, again

From
jian he
Date:
sql-altertable.html

   <varlistentry id="sql-altertable-desc-set-drop-not-null">
    <term><literal>SET</literal>/<literal>DROP NOT NULL</literal></term>
    <listitem>
     <para>
      These forms change whether a column is marked to allow null
      values or to reject null values.
     </para>
     <para>
      If this table is a partition, one cannot perform <literal>DROP
NOT NULL</literal>
      on a column if it is marked <literal>NOT NULL</literal> in the parent
      table.  To drop the <literal>NOT NULL</literal> constraint from all the
      partitions, perform <literal>DROP NOT NULL</literal> on the parent
      table.
     </para>
Now this will be slightly inaccurate.

drop table if exists part, part0 cascade;
create table part (a int not null) partition by range (a);
create table part0 (a int not null);
alter table part attach partition part0 for values from (0) to (1000);
alter table ONLY part0 add primary key(a);
alter table part alter column a drop not null;

as the example shows that part0 not-null constraint is still there.
that means:

perform <literal>DROP NOT NULL</literal> on the parent table
will not drop the <literal>NOT NULL</literal> constraint from all partitions.

so we need rephrase the following sentence:

To drop the <literal>NOT NULL</literal> constraint from all the
      partitions, perform <literal>DROP NOT NULL</literal> on the parent
      table.

to address this kind of corner case?



Re: not null constraints, again

From
jian he
Date:
RemoveInheritance
            if (copy_con->coninhcount <= 0) /* shouldn't happen */
                elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
                     RelationGetRelid(child_rel), NameStr(copy_con->conname));
dropconstraint_internal
        if (childcon->coninhcount <= 0) /* shouldn't happen */
            elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
                 childrelid, NameStr(childcon->conname));

RemoveInheritance error triggered (see below), dropconstraint_internal may also.
that means the error message should use RelationGetRelationName
rather than plain "relation %u"?


drop table if exists inh_parent,inh_child1,inh_child2;
create table inh_parent(f1 int not null no inherit);
create table inh_child1(f1 int not null no inherit);
alter table inh_child1 inherit inh_parent;
alter table inh_child1 NO INHERIT inh_parent;
ERROR:  relation 26387 has non-inherited constraint "inh_child1_f1_not_null"

sql-altertable.html
INHERIT parent_table
        This form adds the target table as a new child of the
specified parent table.
        Subsequently, queries against the parent will include records
of the target
        table.  To be added as a child, the target table must already
contain all the
        same columns as the parent (it could have additional columns,
too).  The columns
        must have matching data types, and if they have NOT NULL
constraints in the
        parent then they must also have NOT NULL constraints in the child.

"
The columns must have matching data types, and if they have NOT NULL
constraints in the
 parent then they must also have NOT NULL constraints in the child.
"
For the above sentence, we need to add some text to explain
NOT NULL constraints, NO INHERIT property
for the child table and parent table.

------------------------------------------------
drop table if exists inh_parent,inh_child1,inh_child2;
create table inh_parent(f1 int not null no inherit);
create table inh_child1(f1 int);
alter table inh_child1 inherit inh_parent;
alter table inh_child1 NO INHERIT inh_parent;
ERROR:  1 unmatched constraints while removing inheritance from
"inh_child1" to "inh_parent"

now, we cannot "uninherit" inh_child1 from inh_parent?
not sure this is expected behavior.



Re: not null constraints, again

From
Alvaro Herrera
Date:
On 2024-Nov-07, jian he wrote:

> RemoveInheritance
>             if (copy_con->coninhcount <= 0) /* shouldn't happen */
>                 elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
>                      RelationGetRelid(child_rel), NameStr(copy_con->conname));
> dropconstraint_internal
>         if (childcon->coninhcount <= 0) /* shouldn't happen */
>             elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
>                  childrelid, NameStr(childcon->conname));
> 
> RemoveInheritance error triggered (see below), dropconstraint_internal may also.
> that means the error message should use RelationGetRelationName
> rather than plain "relation %u"?
> 
> drop table if exists inh_parent,inh_child1,inh_child2;
> create table inh_parent(f1 int not null no inherit);
> create table inh_child1(f1 int not null no inherit);
> alter table inh_child1 inherit inh_parent;
> alter table inh_child1 NO INHERIT inh_parent;
> ERROR:  relation 26387 has non-inherited constraint "inh_child1_f1_not_null"

Hmm, no, this is just a code bug: in RemoveInheritance when scanning
'parent' for constraints, we must skip the ones that are NO INHERIT, but
weren't.  With the bug fixed, the sequence above results in a
no-longer-child inh_child1 that still has inh_child1_f1_not_null, and no
error is thrown.

> sql-altertable.html
> INHERIT parent_table
>         This form adds the target table as a new child of the specified parent table.
>         Subsequently, queries against the parent will include records of the target
>         table.  To be added as a child, the target table must already contain all the
>         same columns as the parent (it could have additional columns, too).  The columns
>         must have matching data types, and if they have NOT NULL constraints in the
>         parent then they must also have NOT NULL constraints in the child.
> 
> "
> The columns must have matching data types, and if they have NOT NULL
> constraints in the
>  parent then they must also have NOT NULL constraints in the child.
> "
> For the above sentence, we need to add some text to explain
> NOT NULL constraints, NO INHERIT property
> for the child table and parent table.

True.  I rewrote as follows, moving the whole explanation of constraints
together to the same paragraph, rather than talking about some
constraints in one paragraph and other constraints in another.  The
previous approach was better when NOT NULL markings were a property of
the column, but now that they are constraints in their own right, this
seems better.

    <term><literal>INHERIT <replaceable class="parameter">parent_table</replaceable></literal></term>
    <listitem>
     <para>
      This form adds the target table as a new child of the specified parent
      table.  Subsequently, queries against the parent will include records
      of the target table.  To be added as a child, the target table must
      already contain all the same columns as the parent (it could have
      additional columns, too).  The columns must have matching data types.
     </para>
      
     <para>
      In addition, all <literal>CHECK</literal> and <literal>NOT NULL</literal>
      constraints on the parent must also exist on the child, except those
      marked non-inheritable (that is, created with
      <literal>ALTER TABLE ... ADD CONSTRAINT ... NO INHERIT</literal>), which
      are ignored.  All child-table constraints matched must not be marked
      non-inheritable.  Currently
      <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, and
      <literal>FOREIGN KEY</literal> constraints are not considered, but
      this might change in the future.
     </para>
    </listitem>


> ------------------------------------------------
> drop table if exists inh_parent,inh_child1,inh_child2;
> create table inh_parent(f1 int not null no inherit);
> create table inh_child1(f1 int);
> alter table inh_child1 inherit inh_parent;
> alter table inh_child1 NO INHERIT inh_parent;
> ERROR:  1 unmatched constraints while removing inheritance from "inh_child1" to "inh_parent"
> 
> now, we cannot "uninherit" inh_child1 from inh_parent?
> not sure this is expected behavior.

Yeah, this is the same bug as above.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"In fact, the basic problem with Perl 5's subroutines is that they're not
crufty enough, so the cruft leaks out into user-defined code instead, by
the Conservation of Cruft Principle."  (Larry Wall, Apocalypse 6)



Re: not null constraints, again

From
jian he
Date:
> Here's v11, which I intended to commit today, but didn't get around to.
> CI is happy with it, so I'll probably do it tomorrow first thing.
>
v11 still has column_constraint versus table_constraint inconsistency.

create table t7 (a int generated by default as identity, constraint
foo not null a no inherit, b int);
create table t7 (a int generated by default as identity not null no
inherit, b int);
create table t8 (a serial, constraint foo1 not null a no inherit);
create table t8 (a serial not null no inherit, b int);

i solved this issue at [1],
that patch has one whitespace issue though.

what do you think?
[1]  https://postgr.es/m/CACJufxHgBsJrHyGJ0EQzi9XV+ZSozNDcUJ5sg-f5Wk+dGCYZMg@mail.gmail.com



Re: not null constraints, again

From
Alvaro Herrera
Date:
On 2024-Nov-08, jian he wrote:

> > Here's v11, which I intended to commit today, but didn't get around to.
> > CI is happy with it, so I'll probably do it tomorrow first thing.
> >
> v11 still has column_constraint versus table_constraint inconsistency.
> 
> create table t7 (a int generated by default as identity, constraint
> foo not null a no inherit, b int);
> create table t7 (a int generated by default as identity not null no
> inherit, b int);
> create table t8 (a serial, constraint foo1 not null a no inherit);
> create table t8 (a serial not null no inherit, b int);
> 
> i solved this issue at [1],

Ah yeah, that stuff.  Your commit message said it was a refactoring so I
hadn't paid too much attention to it, but it's in fact not a refactoring
at all.  I included it with a large comment explaining why we do it that
way and that we may want to remove it in the future.  I also included
these four sentences above in the tests, and pushed it after checking
that the CI results are clean.

Yesterday I verified that pg_upgrade works with the regression database
from 12 onwards.  I know the buildfarm uses a different way to do the
pg_upgrade test, so there's no way to know if it'll work ahead of time.

But we'll see what else the buildfarm has to say now that I pushed it ...

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/



Re: not null constraints, again

From
Tom Lane
Date:
Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
> But we'll see what else the buildfarm has to say now that I pushed it ...

A lot of the buildfarm is saying

 adder         | 2024-11-08 13:04:39 | ../pgsql/src/backend/catalog/pg_constraint.c:708:37: warning: comparison is
alwaystrue due to limited range of data type [-Wtype-limits] 

which evidently is about this:

    Assert(colnum > 0 && colnum <= MaxAttrNumber);

The memcpy right before that doesn't seem like project style either.
Most other places that are doing similar things just cast the
ARR_DATA_PTR to the right pointer type and dereference it.

            regards, tom lane



Re: not null constraints, again

From
Alvaro Herrera
Date:
On 2024-Nov-08, Tom Lane wrote:

> Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
> > But we'll see what else the buildfarm has to say now that I pushed it ...
> 
> A lot of the buildfarm is saying
> 
>  adder         | 2024-11-08 13:04:39 | ../pgsql/src/backend/catalog/pg_constraint.c:708:37: warning: comparison is
alwaystrue due to limited range of data type [-Wtype-limits]
 
> 
> which evidently is about this:
> 
>     Assert(colnum > 0 && colnum <= MaxAttrNumber);

Hah.

> The memcpy right before that doesn't seem like project style either.
> Most other places that are doing similar things just cast the
> ARR_DATA_PTR to the right pointer type and dereference it.

Hmm, yeah, that's easily removed,


diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index e953000c01d..043bf7c24dd 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -704,11 +704,7 @@ extractNotNullColumn(HeapTuple constrTup)
         ARR_DIMS(arr)[0] != 1)
         elog(ERROR, "conkey is not a 1-D smallint array");
 
-    memcpy(&colnum, ARR_DATA_PTR(arr), sizeof(AttrNumber));
-    Assert(colnum > 0 && colnum <= MaxAttrNumber);
-
-    if ((Pointer) arr != DatumGetPointer(adatum))
-        pfree(arr);                /* free de-toasted copy, if any */
+    colnum = ((AttrNumber *) ARR_DATA_PTR(arr))[0];
 
     return colnum;
 }


I notice I cargo-culted a "free de-toasted copy", but I think it's
impossible to end up with a toasted datum here, because the column is
guaranteed to have only one element, so not a candidate for toasting.
But also, if we don't free it (in case somebody does an UPDATE to the
catalog with a large array), nothing happens, because memory is going to
be released soon anyway, by the error that results by conkey not being
one element long.

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/
"Si quieres ser creativo, aprende el arte de perder el tiempo"



Re: not null constraints, again

From
Alvaro Herrera
Date:
On 2024-Nov-09, Alvaro Herrera wrote:

> I notice I cargo-culted a "free de-toasted copy", but I think it's
> impossible to end up with a toasted datum here, because the column is
> guaranteed to have only one element, so not a candidate for toasting.
> But also, if we don't free it (in case somebody does an UPDATE to the
> catalog with a large array), nothing happens, because memory is going to
> be released soon anyway, by the error that results by conkey not being
> one element long.

I found out that my claim that it's impossible to have a detoasted datum
was false: because the value is so small, we end up with a short
varlena, which does use a separate palloc().  I decided to remove the
pfree() anyway, because that makes it easier to return the value we want
without having to first assign it away from the chunk we'd pfree.
The DDL code mostly doesn't worry too much about memory leaks anyway,
and this one is very small.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
“Cuando no hay humildad las personas se degradan” (A. Christie)



Re: not null constraints, again

From
jian he
Date:
hi.

heap_create_with_catalog argument (cooked_constraints):
passed as NIL in function {create_toast_table, make_new_heap}
passed as list_concat(cookedDefaults,old_constraints) in DefineRelation

in DefineRelation we have function call:
MergeAttributes
heap_create_with_catalog
StoreConstraints

StoreConstraints second argument: cooked_constraints, some is comes from
DefineRelation->MergeAttributes old_constraints:
{
stmt->tableElts = MergeAttributes(stmt->tableElts, inheritOids,
stmt->relation->relpersistence, stmt->partbound != NULL, &old_constraints,
&old_notnulls);
}

My understanding from DefineRelation->MergeAttributes is that old_constraints
will only have CHECK constraints.
that means heap_create_with_catalog->StoreConstraints
StoreConstraints didn't actually handle CONSTR_NOTNULL.

heap_create_with_catalog comments also says:
* cooked_constraints: list of precooked check constraints and defaults

coverage https://coverage.postgresql.org/src/backend/catalog/heap.c.gcov.html
also shows StoreConstraints, CONSTR_NOTNULL never being called,
which is added by this thread.


my question is can we remove StoreConstraints, CONSTR_NOTNULL handling.
we have 3 functions {StoreConstraints, AddRelationNotNullConstraints,
AddRelationNewConstraints} that will call StoreRelNotNull to store the not-null
constraint. That means if we want to bullet proof that something is conflicting
with not-null, we need to add code to check all these 3 places.
removing StoreConstraints handling not-null seems helpful.


also comments in MergeAttributes:
 * Output arguments:
 * 'supconstr' receives a list of constraints belonging to the parents,
 *        updated as necessary to be valid for the child.
 * 'supnotnulls' receives a list of CookedConstraints that corresponds to
 *        constraints coming from inheritance parents.

can we be explicit that "supconstr" is only about CHECK constraint,
"supnotnulls" is
only about NOT-NULL constraint.