LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0
 ALTER TABLE regtest_table ALTER b DROP NOT NULL;
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table.b" permissive=0
-LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0
 ALTER TABLE regtest_table ALTER b SET STATISTICS -1;
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table.b" permissive=0
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_2.b" permissive=0
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_1_tens.p" permissive=0
 ALTER TABLE regtest_ptable ALTER p DROP NOT NULL;
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_ptable.p" permissive=0
-LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table_part.p" permissive=0
-LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_ptable_1_tens.p" permissive=0
 ALTER TABLE regtest_ptable ALTER p SET STATISTICS -1;
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_ptable.p" permissive=0
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema_2.regtest_table_part.p" permissive=0
 
 LOG:  SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0
 LOG:  SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=system_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="pg_catalog" permissive=0
 LOG:  SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0
+LOG:  SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0
 LOG:  SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_table name="regtest_schema.regtest_table" permissive=0
 LOG:  SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0
 LOG:  SELinux: allowed { create } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_4.y" permissive=0
 LOG:  SELinux: allowed { create } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_column name="regtest_schema.regtest_table_4.z" permissive=0
 LOG:  SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0
+LOG:  SELinux: allowed { search } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0
 LOG:  SELinux: allowed { add_name } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_schema_t:s0 tclass=db_schema name="regtest_schema" permissive=0
 LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_regtest_superuser_t:s0 tcontext=unconfined_u:object_r:sepgsql_table_t:s0 tclass=db_table name="regtest_schema.regtest_table_4" permissive=0
 CREATE INDEX regtest_index_tbl4_y ON regtest_table_4(y);
 
  options  | text[]  |           |          |                                                  | extended |              | 
 Indexes:
     "replication_metadata_pkey" PRIMARY KEY, btree (id)
+Not-null constraints:
+    "replication_metadata_id_not_null" NOT NULL "id"
+    "replication_metadata_relation_not_null" NOT NULL "relation"
 Options: user_catalog_table=true
 
 INSERT INTO replication_metadata(relation, options)
  options  | text[]  |           |          |                                                  | extended |              | 
 Indexes:
     "replication_metadata_pkey" PRIMARY KEY, btree (id)
+Not-null constraints:
+    "replication_metadata_id_not_null" NOT NULL "id"
+    "replication_metadata_relation_not_null" NOT NULL "relation"
 
 INSERT INTO replication_metadata(relation, options)
 VALUES ('bar', ARRAY['a', 'b']);
  options  | text[]  |           |          |                                                  | extended |              | 
 Indexes:
     "replication_metadata_pkey" PRIMARY KEY, btree (id)
+Not-null constraints:
+    "replication_metadata_id_not_null" NOT NULL "id"
+    "replication_metadata_relation_not_null" NOT NULL "relation"
 Options: user_catalog_table=true
 
 INSERT INTO replication_metadata(relation, options)
  rewritemeornot | integer |           |          |                                                  | plain    |              | 
 Indexes:
     "replication_metadata_pkey" PRIMARY KEY, btree (id)
+Not-null constraints:
+    "replication_metadata_id_not_null" NOT NULL "id"
+    "replication_metadata_relation_not_null" NOT NULL "relation"
 Options: user_catalog_table=false
 
 INSERT INTO replication_metadata(relation, options)
 
        <structfield>attnotnull</structfield> <type>bool</type>
       </para>
       <para>
-       This represents a not-null constraint.
+       This column is marked not-null, either by a not-null constraint
+       or a primary key.
       </para></entry>
      </row>
 
   </indexterm>
 
   <para>
-   The catalog <structname>pg_constraint</structname> stores check, primary
-   key, unique, foreign key, and exclusion constraints on tables.
+   The catalog <structname>pg_constraint</structname> stores check, not-null,
+   primary key, unique, foreign key, and exclusion constraints on tables.
    (Column constraints are not treated specially.  Every column constraint is
    equivalent to some table constraint.)
-   Not-null constraints are represented in the
-   <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>
-   catalog, not here.
   </para>
 
   <para>
       <para>
        <literal>c</literal> = check constraint,
        <literal>f</literal> = foreign key constraint,
+       <literal>n</literal> = not-null constraint,
        <literal>p</literal> = primary key constraint,
        <literal>u</literal> = unique constraint,
        <literal>t</literal> = constraint trigger,
 
     price numeric
 );
 </programlisting>
+    An explicit constraint name can also be specified, for example:
+<programlisting>
+CREATE TABLE products (
+    product_no integer NOT NULL,
+    name text <emphasis>CONSTRAINT products_name_not_null</emphasis> NOT NULL,
+    price numeric
+);
+</programlisting>
+   </para>
+
+   <para>
+    A not-null constraint is usually written as a column constraint.  The
+    syntax for writing it as a table constraint is
+<programlisting>
+CREATE TABLE products (
+    product_no integer,
+    name text,
+    price numeric,
+    <emphasis>NOT NULL product_no</emphasis>,
+    <emphasis>NOT NULL name</emphasis>
+);
+</programlisting>
+    But this syntax is not standard and mainly intended for use by
+    <application>pg_dump</application>.
    </para>
 
    <para>
-    A not-null constraint is always written as a column constraint.  A
-    not-null constraint is functionally equivalent to creating a check
+    A not-null constraint is functionally equivalent to creating a check
     constraint <literal>CHECK (<replaceable>column_name</replaceable>
     IS NOT NULL)</literal>, but in
     <productname>PostgreSQL</productname> creating an explicit
-    not-null constraint is more efficient.  The drawback is that you
-    cannot give explicit names to not-null constraints created this
-    way.
+    not-null constraint is more efficient.
    </para>
 
    <para>
     order the constraints are checked.
    </para>
 
+   <para>
+    However, a column can have at most one explicit not-null constraint.
+   </para>
+
    <para>
     The <literal>NOT NULL</literal> constraint has an inverse: the
     <literal>NULL</literal> constraint.  This does not mean that the
 
    <para>
     A table can have at most one primary key.  (There can be any number
-    of unique and not-null constraints, which are functionally almost the
+    of unique constraints, which combined with not-null constraints are functionally almost the
     same thing, but only one can be identified as the primary key.)
     Relational database theory
     dictates that every table must have a primary key.  This rule is
 ALTER TABLE products ADD CONSTRAINT some_name UNIQUE (product_no);
 ALTER TABLE products ADD FOREIGN KEY (product_group_id) REFERENCES product_groups;
 </programlisting>
-    To add a not-null constraint, which cannot be written as a table
-    constraint, use this syntax:
+   </para>
+
+   <para>
+    To add a not-null constraint, which is normally not written as a table
+    constraint, this special syntax is available:
 <programlisting>
 ALTER TABLE products ALTER COLUMN product_no SET NOT NULL;
 </programlisting>
+    This command silently does nothing if the column already has a
+    not-null constraint.
    </para>
 
    <para>
    </para>
 
    <para>
-    This works the same for all constraint types except not-null
-    constraints. To drop a not null constraint use:
+    Simplified syntax is available to drop a not-null constraint:
 <programlisting>
 ALTER TABLE products ALTER COLUMN product_no DROP NOT NULL;
 </programlisting>
-    (Recall that not-null constraints do not have names.)
+    This mirrors the <literal>SET NOT NULL</literal> syntax for adding a
+    not-null constraint.  This command will silently do nothing if the column
+    does not have a not-null constraint.  (Recall that a column can have at
+    most one not-null constraint, so it is never ambiguous which constraint
+    this command acts on.)
    </para>
   </sect2>
 
 
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
 { CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
+  NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
   UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
   <title>Compatibility</title>
 
   <para>
-   The forms <literal>ADD</literal> (without <literal>USING INDEX</literal>),
+   The forms <literal>ADD [COLUMN]</literal>,
    <literal>DROP [COLUMN]</literal>, <literal>DROP IDENTITY</literal>, <literal>RESTART</literal>,
    <literal>SET DEFAULT</literal>, <literal>SET DATA TYPE</literal> (without <literal>USING</literal>),
    <literal>SET GENERATED</literal>, and <literal>SET <replaceable>sequence_option</replaceable></literal>
-   conform with the SQL standard.  The other forms are
+   conform with the SQL standard.
+   The form <literal>ADD <replaceable>table_constraint</replaceable></literal>
+   conforms with the SQL standard when the <literal>USING INDEX</literal> and
+   <literal>NOT VALID</literal> clauses are omitted and the constraint type is
+   one of <literal>CHECK</literal>, <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>,
+   or <literal>REFERENCES</literal>.
+   The other forms are
    <productname>PostgreSQL</productname> extensions of the SQL standard.
    Also, the ability to specify more than one manipulation in a single
    <command>ALTER TABLE</command> command is an extension.
 
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
 { CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
+  NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
   UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
   EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
     constraint, and index names must be unique across all relations within
     the same schema.
    </para>
-
-   <para>
-    Currently, <productname>PostgreSQL</productname> does not record names
-    for <literal>NOT NULL</literal> constraints at all, so they are not
-    subject to the uniqueness restriction.  This might change in a future
-    release.
-   </para>
   </refsect2>
 
   <refsect2>
 
     */
    attStruct->atttypid = InvalidOid;
 
-   /* Remove any NOT NULL constraint the column may have */
+   /* Remove any not-null constraint the column may have */
    attStruct->attnotnull = false;
 
    /* We don't want to keep stats for it anymore */
    return constrOid;
 }
 
+/*
+ * Store a not-null constraint for the given relation
+ *
+ * The OID of the new constraint is returned.
+ */
+static Oid
+StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
+               bool is_validated, bool is_local, int inhcount,
+               bool is_no_inherit)
+{
+   Oid         constrOid;
+
+   constrOid =
+       CreateConstraintEntry(nnname,
+                             RelationGetNamespace(rel),
+                             CONSTRAINT_NOTNULL,
+                             false,
+                             false,
+                             is_validated,
+                             InvalidOid,
+                             RelationGetRelid(rel),
+                             &attnum,
+                             1,
+                             1,
+                             InvalidOid,   /* not a domain constraint */
+                             InvalidOid,   /* no associated index */
+                             InvalidOid,   /* Foreign key fields */
+                             NULL,
+                             NULL,
+                             NULL,
+                             NULL,
+                             0,
+                             ' ',
+                             ' ',
+                             NULL,
+                             0,
+                             ' ',
+                             NULL, /* not an exclusion constraint */
+                             NULL,
+                             NULL,
+                             is_local,
+                             inhcount,
+                             is_no_inherit,
+                             false);
+   return constrOid;
+}
+
 /*
  * Store defaults and constraints (passed as a list of CookedConstraint).
  *
                                  is_internal);
                numchecks++;
                break;
+
+           case CONSTR_NOTNULL:
+               con->conoid =
+                   StoreRelNotNull(rel, con->name, con->attnum,
+                                   !con->skip_validation, con->is_local,
+                                   con->inhcount, con->is_no_inherit);
+               break;
+
            default:
                elog(ERROR, "unrecognized constraint type: %d",
                     (int) con->contype);
    ParseNamespaceItem *nsitem;
    int         numchecks;
    List       *checknames;
+   List       *nnnames;
    ListCell   *cell;
    Node       *expr;
    CookedConstraint *cooked;
     */
    numchecks = numoldchecks;
    checknames = NIL;
+   nnnames = NIL;
    foreach(cell, newConstraints)
    {
        Constraint *cdef = (Constraint *) lfirst(cell);
-       char       *ccname;
        Oid         constrOid;
 
-       if (cdef->contype != CONSTR_CHECK)
-           continue;
-
-       if (cdef->raw_expr != NULL)
+       if (cdef->contype == CONSTR_CHECK)
        {
-           Assert(cdef->cooked_expr == NULL);
+           char       *ccname;
 
-           /*
-            * Transform raw parsetree to executable expression, and verify
-            * it's valid as a CHECK constraint.
-            */
-           expr = cookConstraint(pstate, cdef->raw_expr,
-                                 RelationGetRelationName(rel));
-       }
-       else
-       {
-           Assert(cdef->cooked_expr != NULL);
+           if (cdef->raw_expr != NULL)
+           {
+               Assert(cdef->cooked_expr == NULL);
+
+               /*
+                * Transform raw parsetree to executable expression, and
+                * verify it's valid as a CHECK constraint.
+                */
+               expr = cookConstraint(pstate, cdef->raw_expr,
+                                     RelationGetRelationName(rel));
+           }
+           else
+           {
+               Assert(cdef->cooked_expr != NULL);
+
+               /*
+                * Here, we assume the parser will only pass us valid CHECK
+                * expressions, so we do no particular checking.
+                */
+               expr = stringToNode(cdef->cooked_expr);
+           }
 
            /*
-            * Here, we assume the parser will only pass us valid CHECK
-            * expressions, so we do no particular checking.
+            * Check name uniqueness, or generate a name if none was given.
             */
-           expr = stringToNode(cdef->cooked_expr);
-       }
+           if (cdef->conname != NULL)
+           {
+               ListCell   *cell2;
 
-       /*
-        * Check name uniqueness, or generate a name if none was given.
-        */
-       if (cdef->conname != NULL)
-       {
-           ListCell   *cell2;
+               ccname = cdef->conname;
+               /* Check against other new constraints */
+               /* Needed because we don't do CommandCounterIncrement in loop */
+               foreach(cell2, checknames)
+               {
+                   if (strcmp((char *) lfirst(cell2), ccname) == 0)
+                       ereport(ERROR,
+                               (errcode(ERRCODE_DUPLICATE_OBJECT),
+                                errmsg("check constraint \"%s\" already exists",
+                                       ccname)));
+               }
 
-           ccname = cdef->conname;
-           /* Check against other new constraints */
-           /* Needed because we don't do CommandCounterIncrement in loop */
-           foreach(cell2, checknames)
-           {
-               if (strcmp((char *) lfirst(cell2), ccname) == 0)
-                   ereport(ERROR,
-                           (errcode(ERRCODE_DUPLICATE_OBJECT),
-                            errmsg("check constraint \"%s\" already exists",
-                                   ccname)));
+               /* save name for future checks */
+               checknames = lappend(checknames, ccname);
+
+               /*
+                * Check against pre-existing constraints.  If we are allowed
+                * to merge with an existing constraint, there's no more to do
+                * here. (We omit the duplicate constraint from the result,
+                * which is what ATAddCheckConstraint wants.)
+                */
+               if (MergeWithExistingConstraint(rel, ccname, expr,
+                                               allow_merge, is_local,
+                                               cdef->initially_valid,
+                                               cdef->is_no_inherit))
+                   continue;
            }
+           else
+           {
+               /*
+                * When generating a name, we want to create "tab_col_check"
+                * for a column constraint and "tab_check" for a table
+                * constraint.  We no longer have any info about the syntactic
+                * positioning of the constraint phrase, so we approximate
+                * this by seeing whether the expression references more than
+                * one column.  (If the user played by the rules, the result
+                * is the same...)
+                *
+                * Note: pull_var_clause() doesn't descend into sublinks, but
+                * we eliminated those above; and anyway this only needs to be
+                * an approximate answer.
+                */
+               List       *vars;
+               char       *colname;
+
+               vars = pull_var_clause(expr, 0);
+
+               /* eliminate duplicates */
+               vars = list_union(NIL, vars);
+
+               if (list_length(vars) == 1)
+                   colname = get_attname(RelationGetRelid(rel),
+                                         ((Var *) linitial(vars))->varattno,
+                                         true);
+               else
+                   colname = NULL;
 
-           /* save name for future checks */
-           checknames = lappend(checknames, ccname);
+               ccname = ChooseConstraintName(RelationGetRelationName(rel),
+                                             colname,
+                                             "check",
+                                             RelationGetNamespace(rel),
+                                             checknames);
+
+               /* save name for future checks */
+               checknames = lappend(checknames, ccname);
+           }
 
            /*
-            * Check against pre-existing constraints.  If we are allowed to
-            * merge with an existing constraint, there's no more to do here.
-            * (We omit the duplicate constraint from the result, which is
-            * what ATAddCheckConstraint wants.)
+            * OK, store it.
             */
-           if (MergeWithExistingConstraint(rel, ccname, expr,
-                                           allow_merge, is_local,
-                                           cdef->initially_valid,
-                                           cdef->is_no_inherit))
-               continue;
+           constrOid =
+               StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
+                             is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
+
+           numchecks++;
+
+           cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
+           cooked->contype = CONSTR_CHECK;
+           cooked->conoid = constrOid;
+           cooked->name = ccname;
+           cooked->attnum = 0;
+           cooked->expr = expr;
+           cooked->skip_validation = cdef->skip_validation;
+           cooked->is_local = is_local;
+           cooked->inhcount = is_local ? 0 : 1;
+           cooked->is_no_inherit = cdef->is_no_inherit;
+           cookedConstraints = lappend(cookedConstraints, cooked);
        }
-       else
+       else if (cdef->contype == CONSTR_NOTNULL)
        {
-           /*
-            * When generating a name, we want to create "tab_col_check" for a
-            * column constraint and "tab_check" for a table constraint.  We
-            * no longer have any info about the syntactic positioning of the
-            * constraint phrase, so we approximate this by seeing whether the
-            * expression references more than one column.  (If the user
-            * played by the rules, the result is the same...)
-            *
-            * Note: pull_var_clause() doesn't descend into sublinks, but we
-            * eliminated those above; and anyway this only needs to be an
-            * approximate answer.
-            */
-           List       *vars;
-           char       *colname;
+           CookedConstraint *nncooked;
+           AttrNumber  colnum;
+           char       *nnname;
 
-           vars = pull_var_clause(expr, 0);
+           /* Determine which column to modify */
+           colnum = get_attnum(RelationGetRelid(rel), strVal(linitial(cdef->keys)));
+           if (colnum == InvalidAttrNumber)    /* shouldn't happen */
+               elog(ERROR, "cache lookup failed for attribute \"%s\" of relation %u",
+                    strVal(linitial(cdef->keys)), RelationGetRelid(rel));
 
-           /* eliminate duplicates */
-           vars = list_union(NIL, vars);
+           /*
+            * If the column already has a not-null constraint, we need only
+            * update its catalog status and we're done.
+            */
+           if (AdjustNotNullInheritance1(RelationGetRelid(rel), colnum,
+                                         cdef->inhcount))
+               continue;
 
-           if (list_length(vars) == 1)
-               colname = get_attname(RelationGetRelid(rel),
-                                     ((Var *) linitial(vars))->varattno,
-                                     true);
+           /*
+            * If a constraint name is specified, check that it isn't already
+            * used.  Otherwise, choose a non-conflicting one ourselves.
+            */
+           if (cdef->conname)
+           {
+               if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
+                                        RelationGetRelid(rel),
+                                        cdef->conname))
+                   ereport(ERROR,
+                           errcode(ERRCODE_DUPLICATE_OBJECT),
+                           errmsg("constraint \"%s\" for relation \"%s\" already exists",
+                                  cdef->conname, RelationGetRelationName(rel)));
+               nnname = cdef->conname;
+           }
            else
-               colname = NULL;
-
-           ccname = ChooseConstraintName(RelationGetRelationName(rel),
-                                         colname,
-                                         "check",
-                                         RelationGetNamespace(rel),
-                                         checknames);
-
-           /* save name for future checks */
-           checknames = lappend(checknames, ccname);
+               nnname = ChooseConstraintName(RelationGetRelationName(rel),
+                                             strVal(linitial(cdef->keys)),
+                                             "not_null",
+                                             RelationGetNamespace(rel),
+                                             nnnames);
+           nnnames = lappend(nnnames, nnname);
+
+           constrOid =
+               StoreRelNotNull(rel, nnname, colnum,
+                               cdef->initially_valid,
+                               cdef->inhcount == 0,
+                               cdef->inhcount,
+                               cdef->is_no_inherit);
+
+           nncooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
+           nncooked->contype = CONSTR_NOTNULL;
+           nncooked->conoid = constrOid;
+           nncooked->name = nnname;
+           nncooked->attnum = colnum;
+           nncooked->expr = NULL;
+           nncooked->skip_validation = cdef->skip_validation;
+           nncooked->is_local = is_local;
+           nncooked->inhcount = cdef->inhcount;
+           nncooked->is_no_inherit = cdef->is_no_inherit;
+
+           cookedConstraints = lappend(cookedConstraints, nncooked);
        }
-
-       /*
-        * OK, store it.
-        */
-       constrOid =
-           StoreRelCheck(rel, ccname, expr, cdef->initially_valid, is_local,
-                         is_local ? 0 : 1, cdef->is_no_inherit, is_internal);
-
-       numchecks++;
-
-       cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
-       cooked->contype = CONSTR_CHECK;
-       cooked->conoid = constrOid;
-       cooked->name = ccname;
-       cooked->attnum = 0;
-       cooked->expr = expr;
-       cooked->skip_validation = cdef->skip_validation;
-       cooked->is_local = is_local;
-       cooked->inhcount = is_local ? 0 : 1;
-       cooked->is_no_inherit = cdef->is_no_inherit;
-       cookedConstraints = lappend(cookedConstraints, cooked);
    }
 
    /*
    return found;
 }
 
+/* list_sort comparator to sort CookedConstraint by attnum */
+static int
+list_cookedconstr_attnum_cmp(const ListCell *p1, const ListCell *p2)
+{
+   AttrNumber  v1 = ((CookedConstraint *) lfirst(p1))->attnum;
+   AttrNumber  v2 = ((CookedConstraint *) lfirst(p2))->attnum;
+
+   if (v1 < v2)
+       return -1;
+   if (v1 > v2)
+       return 1;
+   return 0;
+}
+
+/*
+ * Create the not-null constraints when creating a new relation
+ *
+ * These come from two sources: the 'constraints' list (of Constraint) is
+ * specified directly by the user; the 'old_notnulls' list (of
+ * CookedConstraint) comes from inheritance.  We create one constraint
+ * for each column, giving priority to user-specified ones, and setting
+ * inhcount according to how many parents cause each column to get a
+ * not-null constraint.  If a user-specified name clashes with another
+ * user-specified name, an error is raised.
+ *
+ * Note that inherited constraints have two shapes: those coming from another
+ * not-null constraint in the parent, which have a name already, and those
+ * coming from a primary key in the parent, which don't.  Any name specified
+ * in a parent is disregarded in case of a conflict.
+ *
+ * Returns a list of AttrNumber for columns that need to have the attnotnull
+ * flag set.
+ */
+List *
+AddRelationNotNullConstraints(Relation rel, List *constraints,
+                             List *old_notnulls)
+{
+   List       *givennames;
+   List       *nnnames;
+   List       *nncols = NIL;
+   ListCell   *lc;
+
+   /*
+    * We track two lists of names: nnnames keeps all the constraint names,
+    * givennames tracks user-generated names.  The distinction is important,
+    * because we must raise error for user-generated name conflicts, but for
+    * system-generated name conflicts we just generate another.
+    */
+   nnnames = NIL;
+   givennames = NIL;
+
+   /*
+    * First, create all not-null constraints that are directly specified by
+    * the user.  Note that inheritance might have given us another source for
+    * each, so we must scan the old_notnulls list and increment inhcount for
+    * each element with identical attnum.  We delete from there any element
+    * that we process.
+    */
+   foreach(lc, constraints)
+   {
+       Constraint *constr = lfirst_node(Constraint, lc);
+       AttrNumber  attnum;
+       char       *conname;
+       bool        is_local = true;
+       int         inhcount = 0;
+       ListCell   *lc2;
+
+       Assert(constr->contype == CONSTR_NOTNULL);
+
+       attnum = get_attnum(RelationGetRelid(rel),
+                           strVal(linitial(constr->keys)));
+
+       /*
+        * Search in the list of inherited constraints for any entries on the
+        * same column.
+        */
+       foreach(lc2, old_notnulls)
+       {
+           CookedConstraint *old = (CookedConstraint *) lfirst(lc2);
+
+           if (old->attnum == attnum)
+           {
+               inhcount++;
+               old_notnulls = foreach_delete_current(old_notnulls, lc2);
+           }
+       }
+
+       /*
+        * Determine a constraint name, which may have been specified by the
+        * user, or raise an error if a conflict exists with another
+        * user-specified name.
+        */
+       if (constr->conname)
+       {
+           foreach(lc2, givennames)
+           {
+               if (strcmp(lfirst(lc2), constr->conname) == 0)
+                   ereport(ERROR,
+                           errcode(ERRCODE_DUPLICATE_OBJECT),
+                           errmsg("constraint \"%s\" for relation \"%s\" already exists",
+                                  constr->conname,
+                                  RelationGetRelationName(rel)));
+           }
+
+           conname = constr->conname;
+           givennames = lappend(givennames, conname);
+       }
+       else
+           conname = ChooseConstraintName(RelationGetRelationName(rel),
+                                          get_attname(RelationGetRelid(rel),
+                                                      attnum, false),
+                                          "not_null",
+                                          RelationGetNamespace(rel),
+                                          nnnames);
+       nnnames = lappend(nnnames, conname);
+
+       StoreRelNotNull(rel, conname,
+                       attnum, true, is_local,
+                       inhcount, constr->is_no_inherit);
+
+       nncols = lappend_int(nncols, attnum);
+   }
+
+   /*
+    * If any column remains in the old_notnulls list, we must create a not-
+    * null constraint marked not-local.  Because multiple parents could
+    * specify a not-null constraint for the same column, we must count how
+    * many there are and add to the original inhcount accordingly, deleting
+    * elements we've already processed.  We sort the list to make it easy.
+    *
+    * We don't use foreach() here because we have two nested loops over the
+    * constraint list, with possible element deletions in the inner one. If
+    * we used foreach_delete_current() it could only fix up the state of one
+    * of the loops, so it seems cleaner to use looping over list indexes for
+    * both loops.  Note that any deletion will happen beyond where the outer
+    * loop is, so its index never needs adjustment.
+    */
+   list_sort(old_notnulls, list_cookedconstr_attnum_cmp);
+   for (int outerpos = 0; outerpos < list_length(old_notnulls); outerpos++)
+   {
+       CookedConstraint *cooked;
+       char       *conname = NULL;
+       int         add_inhcount = 0;
+       ListCell   *lc2;
+
+       cooked = (CookedConstraint *) list_nth(old_notnulls, outerpos);
+       Assert(cooked->contype == CONSTR_NOTNULL);
+
+       /*
+        * Preserve the first non-conflicting constraint name we come across,
+        * if any
+        */
+       if (conname == NULL && cooked->name)
+           conname = cooked->name;
+
+       for (int restpos = outerpos + 1; restpos < list_length(old_notnulls);)
+       {
+           CookedConstraint *other;
+
+           other = (CookedConstraint *) list_nth(old_notnulls, restpos);
+           if (other->attnum == cooked->attnum)
+           {
+               if (conname == NULL && other->name)
+                   conname = other->name;
+
+               add_inhcount++;
+               old_notnulls = list_delete_nth_cell(old_notnulls, restpos);
+           }
+           else
+               restpos++;
+       }
+
+       /* If we got a name, make sure it isn't one we've already used */
+       if (conname != NULL)
+       {
+           foreach(lc2, nnnames)
+           {
+               if (strcmp(lfirst(lc2), conname) == 0)
+               {
+                   conname = NULL;
+                   break;
+               }
+           }
+       }
+
+       /* and choose a name, if needed */
+       if (conname == NULL)
+           conname = ChooseConstraintName(RelationGetRelationName(rel),
+                                          get_attname(RelationGetRelid(rel),
+                                                      cooked->attnum, false),
+                                          "not_null",
+                                          RelationGetNamespace(rel),
+                                          nnnames);
+       nnnames = lappend(nnnames, conname);
+
+       StoreRelNotNull(rel, conname, cooked->attnum, true,
+                       cooked->is_local, cooked->inhcount + add_inhcount,
+                       cooked->is_no_inherit);
+
+       nncols = lappend_int(nncols, cooked->attnum);
+   }
+
+   return nncols;
+}
+
 /*
  * Update the count of constraints in the relation's pg_class tuple.
  *
 
 #include "access/xact.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
+#include "catalog/heap.h"
 #include "catalog/indexing.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_constraint.h"
    return conname;
 }
 
+/*
+ * Find and return the pg_constraint tuple that implements a validated
+ * not-null constraint for the given column of the given relation.
+ *
+ * XXX This would be easier if we had pg_attribute.notnullconstr with the OID
+ * of the constraint that implements the not-null constraint for that column.
+ * I'm not sure it's worth the catalog bloat and de-normalization, however.
+ */
+HeapTuple
+findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
+{
+   Relation    pg_constraint;
+   HeapTuple   conTup,
+               retval = NULL;
+   SysScanDesc scan;
+   ScanKeyData key;
+
+   pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+   ScanKeyInit(&key,
+               Anum_pg_constraint_conrelid,
+               BTEqualStrategyNumber, F_OIDEQ,
+               ObjectIdGetDatum(relid));
+   scan = systable_beginscan(pg_constraint, ConstraintRelidTypidNameIndexId,
+                             true, NULL, 1, &key);
+
+   while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+   {
+       Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+       AttrNumber  conkey;
+
+       /*
+        * We're looking for a NOTNULL constraint that's marked validated,
+        * with the column we're looking for as the sole element in conkey.
+        */
+       if (con->contype != CONSTRAINT_NOTNULL)
+           continue;
+       if (!con->convalidated)
+           continue;
+
+       conkey = extractNotNullColumn(conTup);
+       if (conkey != attnum)
+           continue;
+
+       /* Found it */
+       retval = heap_copytuple(conTup);
+       break;
+   }
+
+   systable_endscan(scan);
+   table_close(pg_constraint, AccessShareLock);
+
+   return retval;
+}
+
+/*
+ * Find and return the pg_constraint tuple that implements a validated
+ * not-null constraint for the given column of the given relation.
+ */
+HeapTuple
+findNotNullConstraint(Oid relid, const char *colname)
+{
+   AttrNumber  attnum = get_attnum(relid, colname);
+
+   return findNotNullConstraintAttnum(relid, attnum);
+}
+
+/*
+ * Given a pg_constraint tuple for a not-null constraint, return the column
+ * number it is for.
+ */
+AttrNumber
+extractNotNullColumn(HeapTuple constrTup)
+{
+   AttrNumber  colnum;
+   Datum       adatum;
+   ArrayType  *arr;
+
+   /* only tuples for not-null constraints should be given */
+   Assert(((Form_pg_constraint) GETSTRUCT(constrTup))->contype == CONSTRAINT_NOTNULL);
+
+   adatum = SysCacheGetAttrNotNull(CONSTROID, constrTup,
+                                   Anum_pg_constraint_conkey);
+   arr = DatumGetArrayTypeP(adatum);   /* ensure not toasted */
+   if (ARR_NDIM(arr) != 1 ||
+       ARR_HASNULL(arr) ||
+       ARR_ELEMTYPE(arr) != INT2OID ||
+       ARR_DIMS(arr)[0] != 1)
+       elog(ERROR, "conkey is not a 1-D smallint array");
+
+   memcpy(&colnum, ARR_DATA_PTR(arr), sizeof(AttrNumber));
+
+   if ((Pointer) arr != DatumGetPointer(adatum))
+       pfree(arr);             /* free de-toasted copy, if any */
+
+   return colnum;
+}
+
+/*
+ * AdjustNotNullInheritance1
+ *     Adjust inheritance count for a single not-null constraint
+ *
+ * Adjust inheritance count, and possibly islocal status, for the not-null
+ * constraint row of the given column, if it exists, and return true.
+ * If no not-null constraint is found for the column, return false.
+ */
+bool
+AdjustNotNullInheritance1(Oid relid, AttrNumber attnum, int count)
+{
+   HeapTuple   tup;
+
+   tup = findNotNullConstraintAttnum(relid, attnum);
+   if (HeapTupleIsValid(tup))
+   {
+       Relation    pg_constraint;
+       Form_pg_constraint conform;
+
+       pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock);
+       conform = (Form_pg_constraint) GETSTRUCT(tup);
+       if (count > 0)
+           conform->coninhcount += count;
+
+       /* sanity check */
+       if (conform->coninhcount < 0)
+           elog(ERROR, "invalid inhcount %d for constraint \"%s\" on relation \"%s\"",
+                conform->coninhcount, NameStr(conform->conname),
+                get_rel_name(relid));
+
+       /*
+        * If the constraints are no longer inherited, mark them local.  It's
+        * arguable that we should drop them instead, but it's hard to see
+        * that being better.  The user can drop it manually later.
+        */
+       if (conform->coninhcount == 0)
+           conform->conislocal = true;
+
+       CatalogTupleUpdate(pg_constraint, &tup->t_self, tup);
+
+       table_close(pg_constraint, RowExclusiveLock);
+
+       return true;
+   }
+
+   return false;
+}
+
+/*
+ * AdjustNotNullInheritance
+ *     Adjust not-null constraints' inhcount/islocal for
+ *     ALTER TABLE [NO] INHERITS
+ *
+ * Mark the NOT NULL constraints for the given relation columns as
+ * inherited, so that they can't be dropped.
+ *
+ * Caller must have checked beforehand that attnotnull was set for all
+ * columns.  However, some of those could be set because of a primary
+ * key, so throw a proper user-visible error if one is not found.
+ */
+void
+AdjustNotNullInheritance(Oid relid, Bitmapset *columns, int count)
+{
+   Relation    pg_constraint;
+   int         attnum;
+
+   pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock);
+
+   /*
+    * Scan the set of columns and bump inhcount for each.
+    */
+   attnum = -1;
+   while ((attnum = bms_next_member(columns, attnum)) >= 0)
+   {
+       HeapTuple   tup;
+       Form_pg_constraint conform;
+
+       tup = findNotNullConstraintAttnum(relid, attnum);
+       if (!HeapTupleIsValid(tup))
+           ereport(ERROR,
+                   errcode(ERRCODE_DATATYPE_MISMATCH),
+                   errmsg("column \"%s\" in child table must be marked NOT NULL",
+                          get_attname(relid, attnum,
+                                      false)));
+
+       conform = (Form_pg_constraint) GETSTRUCT(tup);
+       conform->coninhcount += count;
+       if (conform->coninhcount < 0)
+           elog(ERROR, "invalid inhcount %d for constraint \"%s\" on relation \"%s\"",
+                conform->coninhcount, NameStr(conform->conname),
+                get_rel_name(relid));
+
+       /*
+        * If the constraints are no longer inherited, mark them local.  It's
+        * arguable that we should drop them instead, but it's hard to see
+        * that being better.  The user can drop it manually later.
+        */
+       if (conform->coninhcount == 0)
+           conform->conislocal = true;
+
+       CatalogTupleUpdate(pg_constraint, &tup->t_self, tup);
+   }
+
+   table_close(pg_constraint, RowExclusiveLock);
+}
+
+/*
+ * RelationGetNotNullConstraints
+ *     Return the list of not-null constraints for the given rel
+ *
+ * Caller can request cooked constraints, or raw.
+ *
+ * This is seldom needed, so we just scan pg_constraint each time.
+ *
+ * XXX This is only used to create derived tables, so NO INHERIT constraints
+ * are always skipped.
+ */
+List *
+RelationGetNotNullConstraints(Oid relid, bool cooked)
+{
+   List       *notnulls = NIL;
+   Relation    constrRel;
+   HeapTuple   htup;
+   SysScanDesc conscan;
+   ScanKeyData skey;
+
+   constrRel = table_open(ConstraintRelationId, AccessShareLock);
+   ScanKeyInit(&skey,
+               Anum_pg_constraint_conrelid,
+               BTEqualStrategyNumber, F_OIDEQ,
+               ObjectIdGetDatum(relid));
+   conscan = systable_beginscan(constrRel, ConstraintRelidTypidNameIndexId, true,
+                                NULL, 1, &skey);
+
+   while (HeapTupleIsValid(htup = systable_getnext(conscan)))
+   {
+       Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(htup);
+       AttrNumber  colnum;
+
+       if (conForm->contype != CONSTRAINT_NOTNULL)
+           continue;
+       if (conForm->connoinherit)
+           continue;
+
+       colnum = extractNotNullColumn(htup);
+
+       if (cooked)
+       {
+           CookedConstraint *cooked;
+
+           cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
+
+           cooked->contype = CONSTR_NOTNULL;
+           cooked->name = pstrdup(NameStr(conForm->conname));
+           cooked->attnum = colnum;
+           cooked->expr = NULL;
+           cooked->skip_validation = false;
+           cooked->is_local = true;
+           cooked->inhcount = 0;
+           cooked->is_no_inherit = conForm->connoinherit;
+
+           notnulls = lappend(notnulls, cooked);
+       }
+       else
+       {
+           Constraint *constr;
+
+           constr = makeNode(Constraint);
+           constr->contype = CONSTR_NOTNULL;
+           constr->conname = pstrdup(NameStr(conForm->conname));
+           constr->deferrable = false;
+           constr->initdeferred = false;
+           constr->location = -1;
+           constr->keys = list_make1(makeString(get_attname(relid, colnum,
+                                                            false)));
+           constr->skip_validation = false;
+           constr->initially_valid = true;
+           notnulls = lappend(notnulls, constr);
+       }
+   }
+
+   systable_endscan(conscan);
+   table_close(constrRel, AccessShareLock);
+
+   return notnulls;
+}
+
+
 /*
  * Delete a single constraint record.
  */
 
 } AlteredTableInfo;
 
 /* Struct describing one new constraint to check in Phase 3 scan */
-/* Note: new NOT NULL constraints are handled elsewhere */
+/* Note: new not-null constraints are handled elsewhere */
 typedef struct NewConstraint
 {
    char       *name;           /* Constraint name, or NULL if none */
 static void RangeVarCallbackForTruncate(const RangeVar *relation,
                                        Oid relId, Oid oldRelId, void *arg);
 static List *MergeAttributes(List *schema, List *supers, char relpersistence,
-                            bool is_partition, List **supconstr);
+                            bool is_partition, List **supconstr,
+                            List **supnotnulls);
 static bool MergeCheckConstraint(List *constraints, char *name, Node *expr);
 static void MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel);
 static void MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel);
                                            bool if_not_exists);
 static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid);
 static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid);
-static void ATPrepDropNotNull(Relation rel, bool recurse, bool recursing);
-static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode);
-static void ATPrepSetNotNull(List **wqueue, Relation rel,
-                            AlterTableCmd *cmd, bool recurse, bool recursing,
-                            LOCKMODE lockmode,
-                            AlterTableUtilityContext *context);
-static ObjectAddress ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
-                                     const char *colName, LOCKMODE lockmode);
-static void ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel,
-                              const char *colName, LOCKMODE lockmode);
+static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
+                                      LOCKMODE lockmode);
+static bool set_attnotnull(List **wqueue, Relation rel,
+                          AttrNumber attnum, bool recurse, LOCKMODE lockmode);
+static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
+                                     char *constrname, char *colName,
+                                     bool recurse, bool recursing,
+                                     List **readyRels, LOCKMODE lockmode);
+static ObjectAddress ATExecSetAttNotNull(List **wqueue, Relation rel,
+                                        const char *colName, LOCKMODE lockmode);
 static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr);
 static bool ConstraintImpliedByRelConstraint(Relation scanrel,
                                             List *testConstraint, List *provenConstraint);
                                      bool recurse, bool recursing,
                                      bool missing_ok, LOCKMODE lockmode,
                                      ObjectAddresses *addrs);
+static void ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
+                               LOCKMODE lockmode, AlterTableUtilityContext *context);
 static ObjectAddress ATExecAddIndex(AlteredTableInfo *tab, Relation rel,
                                    IndexStmt *stmt, bool is_rebuild, LOCKMODE lockmode);
 static ObjectAddress ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
 static char *ChooseForeignKeyConstraintNameAddition(List *colnames);
 static ObjectAddress ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel,
                                              IndexStmt *stmt, LOCKMODE lockmode);
-static ObjectAddress ATAddCheckConstraint(List **wqueue,
-                                         AlteredTableInfo *tab, Relation rel,
-                                         Constraint *constr,
-                                         bool recurse, bool recursing, bool is_readd,
-                                         LOCKMODE lockmode);
+static ObjectAddress ATAddCheckNNConstraint(List **wqueue,
+                                           AlteredTableInfo *tab, Relation rel,
+                                           Constraint *constr,
+                                           bool recurse, bool recursing, bool is_readd,
+                                           LOCKMODE lockmode);
 static ObjectAddress ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab,
                                               Relation rel, Constraint *fkconstraint,
                                               bool recurse, bool recursing,
                                 DropBehavior behavior,
                                 bool recurse, bool recursing,
                                 bool missing_ok, LOCKMODE lockmode);
+static ObjectAddress dropconstraint_internal(Relation rel,
+                                            HeapTuple constraintTup, DropBehavior behavior,
+                                            bool recurse, bool recursing,
+                                            bool missing_ok, List **readyRels,
+                                            LOCKMODE lockmode);
 static void ATPrepAlterColumnType(List **wqueue,
                                  AlteredTableInfo *tab, Relation rel,
                                  bool recurse, bool recursing,
 static void CreateInheritance(Relation child_rel, Relation parent_rel);
 static void RemoveInheritance(Relation child_rel, Relation parent_rel,
                              bool expect_detached);
+static void ATInheritAdjustNotNulls(Relation parent_rel, Relation child_rel,
+                                   int inhcount);
 static ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel,
                                           PartitionCmd *cmd,
                                           AlterTableUtilityContext *context);
-static void AttachPartitionEnsureIndexes(Relation rel, Relation attachrel);
+static void AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel);
 static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
                                               List *partConstraint,
                                               bool validate_default);
 static void validatePartitionedIndex(Relation partedIdx, Relation partedTbl);
 static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx,
                                  Relation partitionTbl);
+static void verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partIdx);
 static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
    TupleDesc   descriptor;
    List       *inheritOids;
    List       *old_constraints;
+   List       *old_notnulls;
    List       *rawDefaults;
    List       *cookedDefaults;
+   List       *nncols;
    Datum       reloptions;
    ListCell   *listptr;
    AttrNumber  attnum;
        MergeAttributes(stmt->tableElts, inheritOids,
                        stmt->relation->relpersistence,
                        stmt->partbound != NULL,
-                       &old_constraints);
+                       &old_constraints, &old_notnulls);
 
    /*
     * Create a tuple descriptor from the relation schema.  Note that this
-    * deals with column names, types, and NOT NULL constraints, but not
-    * default values or CHECK constraints; we handle those below.
+    * deals with column names, types, and in-descriptor NOT NULL flags, but
+    * not default values, NOT NULL or CHECK constraints; we handle those
+    * below.
     */
    descriptor = BuildDescForRelation(stmt->tableElts);
 
        AddRelationNewConstraints(rel, NIL, stmt->constraints,
                                  true, true, false, queryString);
 
+   /*
+    * Finally, merge the not-null constraints that are declared directly with
+    * those that come from parent relations (making sure to count inheritance
+    * appropriately for each), create them, and set the attnotnull flag on
+    * columns that don't yet have it.
+    */
+   nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
+                                          old_notnulls);
+   foreach(listptr, nncols)
+       set_attnotnull(NULL, rel, lfirst_int(listptr), false, NoLock);
+
    ObjectAddressSet(address, RelationRelationId, relationId);
 
    /*
  * 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.
  *
  * Return value:
  * Completed schema list.
  *    If the same attribute name appears multiple times, then it appears
  *    in the result table in the proper location for its first appearance.
  *
- *    Constraints (including NOT NULL constraints) for the child table
+ *    Constraints (including not-null constraints) for the child table
  *    are the union of all relevant constraints, from both the child schema
- *    and parent tables.
+ *    and parent tables.  In addition, in legacy inheritance, each column that
+ *    appears in a primary key in any of the parents also gets a NOT NULL
+ *    constraint (partitioning doesn't need this, because the PK itself gets
+ *    inherited.)
  *
  *    The default value for a child column is defined as:
  *     (1) If the child schema specifies a default, that value is used.
  */
 static List *
 MergeAttributes(List *schema, List *supers, char relpersistence,
-               bool is_partition, List **supconstr)
+               bool is_partition, List **supconstr, List **supnotnulls)
 {
    List       *inhSchema = NIL;
    List       *constraints = NIL;
+   List       *nnconstraints = NIL;
    bool        have_bogus_defaults = false;
    int         child_attno;
    static Node bogus_marker = {0}; /* marks conflicting defaults */
        AttrMap    *newattmap;
        List       *inherited_defaults;
        List       *cols_with_defaults;
+       List       *nnconstrs;
        AttrNumber  parent_attno;
        ListCell   *lc1;
        ListCell   *lc2;
+       Bitmapset  *pkattrs;
+       Bitmapset  *nncols = NULL;
 
        /* caller already got lock */
        relation = table_open(parent, NoLock);
        /* We can't process inherited defaults until newattmap is complete. */
        inherited_defaults = cols_with_defaults = NIL;
 
+       /*
+        * All columns that are part of the parent's primary key need to be
+        * NOT NULL; if partition just the attnotnull bit, otherwise a full
+        * constraint (if they don't have one already).  Also, we request
+        * attnotnull on columns that have a not-null constraint that's not
+        * marked NO INHERIT.
+        */
+       pkattrs = RelationGetIndexAttrBitmap(relation,
+                                            INDEX_ATTR_BITMAP_PRIMARY_KEY);
+       nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation), true);
+       foreach(lc1, nnconstrs)
+           nncols = bms_add_member(nncols,
+                                   ((CookedConstraint *) lfirst(lc1))->attnum);
+
        for (parent_attno = 1; parent_attno <= tupleDesc->natts;
             parent_attno++)
        {
                }
 
                /*
-                * Merge of NOT NULL constraints = OR 'em together
+                * In regular inheritance, columns in the parent's primary key
+                * get an extra not-null constraint.  Partitioning doesn't
+                * need this, because the PK itself is going to be cloned to
+                * the partition.
                 */
-               def->is_not_null |= attribute->attnotnull;
+               if (!is_partition &&
+                   bms_is_member(parent_attno - FirstLowInvalidHeapAttributeNumber,
+                                 pkattrs))
+               {
+                   CookedConstraint *nn;
+
+                   nn = palloc(sizeof(CookedConstraint));
+                   nn->contype = CONSTR_NOTNULL;
+                   nn->conoid = InvalidOid;
+                   nn->name = NULL;
+                   nn->attnum = exist_attno;
+                   nn->expr = NULL;
+                   nn->skip_validation = false;
+                   nn->is_local = false;
+                   nn->inhcount = 1;
+                   nn->is_no_inherit = false;
+
+                   nnconstraints = lappend(nnconstraints, nn);
+               }
+
+               /*
+                * mark attnotnull if parent has it and it's not NO INHERIT
+                */
+               if (bms_is_member(parent_attno, nncols) ||
+                   bms_is_member(parent_attno - FirstLowInvalidHeapAttributeNumber,
+                                 pkattrs))
+                   def->is_not_null = true;
 
                /*
                 * Check for GENERATED conflicts
                                                    attribute->atttypmod);
                def->inhcount = 1;
                def->is_local = false;
-               def->is_not_null = attribute->attnotnull;
+               /* mark attnotnull if parent has it and it's not NO INHERIT */
+               if (bms_is_member(parent_attno, nncols) ||
+                   bms_is_member(parent_attno - FirstLowInvalidHeapAttributeNumber,
+                                 pkattrs))
+                   def->is_not_null = true;
                def->is_from_type = false;
                def->storage = attribute->attstorage;
                def->raw_default = NULL;
                    def->compression = NULL;
                inhSchema = lappend(inhSchema, def);
                newattmap->attnums[parent_attno - 1] = ++child_attno;
+
+               /*
+                * In regular inheritance, columns in the parent's primary key
+                * get an extra not-null constraint.  Partitioning doesn't
+                * need this, because the PK itself is going to be cloned to
+                * the partition.
+                */
+               if (!is_partition &&
+                   bms_is_member(parent_attno -
+                                 FirstLowInvalidHeapAttributeNumber,
+                                 pkattrs))
+               {
+                   CookedConstraint *nn;
+
+                   nn = palloc(sizeof(CookedConstraint));
+                   nn->contype = CONSTR_NOTNULL;
+                   nn->conoid = InvalidOid;
+                   nn->name = NULL;
+                   nn->attnum = newattmap->attnums[parent_attno - 1];
+                   nn->expr = NULL;
+                   nn->skip_validation = false;
+                   nn->is_local = false;
+                   nn->inhcount = 1;
+                   nn->is_no_inherit = false;
+
+                   nnconstraints = lappend(nnconstraints, nn);
+               }
            }
 
            /*
            }
        }
 
+       /*
+        * Also copy the not-null constraints from this parent.  The
+        * attnotnull markings were already installed above.
+        */
+       foreach(lc1, nnconstrs)
+       {
+           CookedConstraint *nn = lfirst(lc1);
+
+           Assert(nn->contype == CONSTR_NOTNULL);
+
+           nn->attnum = newattmap->attnums[nn->attnum - 1];
+           nn->is_local = false;
+           nn->inhcount = 1;
+
+           nnconstraints = lappend(nnconstraints, nn);
+       }
+
        free_attrmap(newattmap);
 
        /*
                }
 
                /*
-                * Merge of NOT NULL constraints = OR 'em together
+                * Merge of not-null constraints = OR 'em together
                 */
                def->is_not_null |= newdef->is_not_null;
 
    /*
     * Now that we have the column definition list for a partition, we can
     * check whether the columns referenced in the column constraint specs
-    * actually exist.  Also, we merge parent's NOT NULL constraints and
-    * defaults into each corresponding column definition.
+    * actually exist.  Also, merge column defaults.
     */
    if (is_partition)
    {
                if (strcmp(coldef->colname, restdef->colname) == 0)
                {
                    found = true;
-                   coldef->is_not_null |= restdef->is_not_null;
 
                    /*
                     * Check for conflicts related to generated columns.
    }
 
    *supconstr = constraints;
+   *supnotnulls = nnconstraints;
+
    return schema;
 }
 
             constraintOid);
    con = (Form_pg_constraint) GETSTRUCT(tuple);
 
-   if (myrelid && con->contype == CONSTRAINT_CHECK && !con->connoinherit)
+   if (myrelid &&
+       (con->contype == CONSTRAINT_CHECK ||
+        con->contype == CONSTRAINT_NOTNULL) &&
+       !con->connoinherit)
    {
        if (recurse)
        {
            case AT_AddIndexConstraint:
            case AT_ReplicaIdentity:
            case AT_SetNotNull:
+           case AT_SetAttNotNull:
            case AT_EnableRowSecurity:
            case AT_DisableRowSecurity:
            case AT_ForceRowSecurity:
                cmd_lockmode = ShareUpdateExclusiveLock;
                break;
 
-           case AT_CheckNotNull:
-
-               /*
-                * This only examines the table's schema; but lock must be
-                * strong enough to prevent concurrent DROP NOT NULL.
-                */
-               cmd_lockmode = AccessShareLock;
-               break;
-
            default:            /* oops */
                elog(ERROR, "unrecognized alter table type: %d",
                     (int) cmd->subtype);
            break;
        case AT_DropNotNull:    /* ALTER COLUMN DROP NOT NULL */
            ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
-           ATPrepDropNotNull(rel, recurse, recursing);
-           ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
+           /* Set up recursion for phase 2; no other prep needed */
+           if (recurse)
+               cmd->recurse = true;
            pass = AT_PASS_DROP;
            break;
        case AT_SetNotNull:     /* ALTER COLUMN SET NOT NULL */
            ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
-           /* Need command-specific recursion decision */
-           ATPrepSetNotNull(wqueue, rel, cmd, recurse, recursing,
-                            lockmode, context);
+           /* Set up recursion for phase 2; no other prep needed */
+           if (recurse)
+               cmd->recurse = true;
            pass = AT_PASS_COL_ATTRS;
            break;
-       case AT_CheckNotNull:   /* check column is already marked NOT NULL */
+       case AT_SetAttNotNull:  /* set pg_attribute.attnotnull without adding
+                                * a constraint */
            ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+           /* Need command-specific recursion decision */
            ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
-           /* No command-specific prep needed */
            pass = AT_PASS_COL_ATTRS;
            break;
        case AT_DropExpression: /* ALTER COLUMN DROP EXPRESSION */
            address = ATExecDropIdentity(rel, cmd->name, cmd->missing_ok, lockmode);
            break;
        case AT_DropNotNull:    /* ALTER COLUMN DROP NOT NULL */
-           address = ATExecDropNotNull(rel, cmd->name, lockmode);
+           address = ATExecDropNotNull(rel, cmd->name, cmd->recurse, lockmode);
            break;
        case AT_SetNotNull:     /* ALTER COLUMN SET NOT NULL */
-           address = ATExecSetNotNull(tab, rel, cmd->name, lockmode);
+           address = ATExecSetNotNull(wqueue, rel, NULL, cmd->name,
+                                      cmd->recurse, false, NULL, lockmode);
            break;
-       case AT_CheckNotNull:   /* check column is already marked NOT NULL */
-           ATExecCheckNotNull(tab, rel, cmd->name, lockmode);
+       case AT_SetAttNotNull:  /* set pg_attribute.attnotnull */
+           address = ATExecSetAttNotNull(wqueue, rel, cmd->name, lockmode);
            break;
        case AT_DropExpression:
            address = ATExecDropExpression(rel, cmd->name, cmd->missing_ok, lockmode);
         */
        switch (cmd2->subtype)
        {
-           case AT_SetNotNull:
-               /* Need command-specific recursion decision */
-               ATPrepSetNotNull(wqueue, rel, cmd2,
-                                recurse, false,
-                                lockmode, context);
+           case AT_SetAttNotNull:
+               ATSimpleRecursion(wqueue, rel, cmd2, recurse, lockmode, context);
                pass = AT_PASS_COL_ATTRS;
                break;
            case AT_AddIndex:
-               /* This command never recurses */
-               /* No command-specific prep needed */
+
+               /*
+                * A primary key on a inheritance parent needs supporting NOT
+                * NULL constraint on its children; enqueue commands to create
+                * those or mark them inherited if they already exist.
+                */
+               ATPrepAddPrimaryKey(wqueue, rel, cmd2, lockmode, context);
                pass = AT_PASS_ADD_INDEX;
                break;
            case AT_AddIndexConstraint:
-               /* This command never recurses */
-               /* No command-specific prep needed */
+               /* as above */
+               ATPrepAddPrimaryKey(wqueue, rel, cmd2, lockmode, context);
                pass = AT_PASS_ADD_INDEXCONSTR;
                break;
            case AT_AddConstraint:
    {
        /*
         * If we are rebuilding the tuples OR if we added any new but not
-        * verified NOT NULL constraints, check all not-null constraints. This
+        * verified not-null constraints, check all not-null constraints. This
         * is a bit of overkill but it minimizes risk of bugs, and
         * heap_attisnull is a pretty cheap test anyway.
         */
                                            RelationGetRelationName(oldrel)),
                                     errtableconstraint(oldrel, con->name)));
                        break;
+                   case CONSTR_NOTNULL:
                    case CONSTR_FOREIGN:
                        /* Nothing to do here */
                        break;
            return "ALTER COLUMN ... DROP NOT NULL";
        case AT_SetNotNull:
            return "ALTER COLUMN ... SET NOT NULL";
+       case AT_SetAttNotNull:
+           return NULL;        /* not real grammar */
        case AT_DropExpression:
            return "ALTER COLUMN ... DROP EXPRESSION";
-       case AT_CheckNotNull:
-           return NULL;        /* not real grammar */
        case AT_SetStatistics:
            return "ALTER COLUMN ... SET STATISTICS";
        case AT_SetOptions:
  */
 static ObjectAddress
 ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
-               AlterTableCmd **cmd,
-               bool recurse, bool recursing,
+               AlterTableCmd **cmd, bool recurse, bool recursing,
                LOCKMODE lockmode, int cur_pass,
                AlterTableUtilityContext *context)
 {
     * the effect of NULL values in the new column.
     *
     * An exception occurs when the new column is of a domain type: the domain
-    * might have a NOT NULL constraint, or a check constraint that indirectly
+    * might have a not-null constraint, or a check constraint that indirectly
     * rejects nulls.  If there are any domain constraints then we construct
     * an explicit NULL default value that will be passed through
     * CoerceToDomain processing.  (This is a tad inefficient, since it causes
 
 /*
  * ALTER TABLE ALTER COLUMN DROP NOT NULL
- */
-
-static void
-ATPrepDropNotNull(Relation rel, bool recurse, bool recursing)
-{
-   /*
-    * If the parent is a partitioned table, like check constraints, we do not
-    * support removing the NOT NULL while partitions exist.
-    */
-   if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
-   {
-       PartitionDesc partdesc = RelationGetPartitionDesc(rel, true);
-
-       Assert(partdesc != NULL);
-       if (partdesc->nparts > 0 && !recurse && !recursing)
-           ereport(ERROR,
-                   (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-                    errmsg("cannot remove constraint from only the partitioned table when partitions exist"),
-                    errhint("Do not specify the ONLY keyword.")));
-   }
-}
-
-/*
+ *
  * Return the address of the modified column.  If the column was already
  * nullable, InvalidObjectAddress is returned.
  */
 static ObjectAddress
-ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode)
+ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
+                 LOCKMODE lockmode)
 {
    HeapTuple   tuple;
+   HeapTuple   conTup;
    Form_pg_attribute attTup;
    AttrNumber  attnum;
    Relation    attr_rel;
-   List       *indexoidlist;
-   ListCell   *indexoidscan;
    ObjectAddress address;
+   List       *readyRels;
 
    /*
     * lookup the attribute
                        colName, RelationGetRelationName(rel))));
    attTup = (Form_pg_attribute) GETSTRUCT(tuple);
    attnum = attTup->attnum;
+   ObjectAddressSubSet(address, RelationRelationId,
+                       RelationGetRelid(rel), attnum);
+
+   /* If the column is already nullable there's nothing to do. */
+   if (!attTup->attnotnull)
+   {
+       table_close(attr_rel, RowExclusiveLock);
+       return InvalidObjectAddress;
+   }
 
    /* Prevent them from altering a system attribute */
    if (attnum <= 0)
                        colName, RelationGetRelationName(rel))));
 
    /*
-    * Check that the attribute is not in a primary key or in an index used as
-    * a replica identity.
-    *
-    * Note: we'll throw error even if the pkey index is not valid.
+    * It's not OK to remove a constraint only for the parent and leave it in
+    * the children, so disallow that.
     */
-
-   /* Loop over all indexes on the relation */
-   indexoidlist = RelationGetIndexList(rel);
-
-   foreach(indexoidscan, indexoidlist)
+   if (!recurse)
    {
-       Oid         indexoid = lfirst_oid(indexoidscan);
-       HeapTuple   indexTuple;
-       Form_pg_index indexStruct;
-       int         i;
+       if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+       {
+           PartitionDesc partdesc;
 
-       indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
-       if (!HeapTupleIsValid(indexTuple))
-           elog(ERROR, "cache lookup failed for index %u", indexoid);
-       indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
+           partdesc = RelationGetPartitionDesc(rel, true);
 
-       /*
-        * If the index is not a primary key or an index used as replica
-        * identity, skip the check.
-        */
-       if (indexStruct->indisprimary || indexStruct->indisreplident)
+           if (partdesc->nparts > 0)
+               ereport(ERROR,
+                       errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("cannot remove constraint from only the partitioned table when partitions exist"),
+                       errhint("Do not specify the ONLY keyword."));
+       }
+       else if (rel->rd_rel->relhassubclass &&
+                find_inheritance_children(RelationGetRelid(rel), NoLock) != NIL)
        {
-           /*
-            * Loop over each attribute in the primary key or the index used
-            * as replica identity and see if it matches the to-be-altered
-            * attribute.
-            */
-           for (i = 0; i < indexStruct->indnkeyatts; i++)
-           {
-               if (indexStruct->indkey.values[i] == attnum)
-               {
-                   if (indexStruct->indisprimary)
-                       ereport(ERROR,
-                               (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-                                errmsg("column \"%s\" is in a primary key",
-                                       colName)));
-                   else
-                       ereport(ERROR,
-                               (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-                                errmsg("column \"%s\" is in index used as replica identity",
-                                       colName)));
-               }
-           }
+           ereport(ERROR,
+                   errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                   errmsg("not-null constraint on column \"%s\" must be removed in child tables too",
+                          colName),
+                   errhint("Do not specify the ONLY keyword."));
        }
-
-       ReleaseSysCache(indexTuple);
    }
 
-   list_free(indexoidlist);
-
-   /* If rel is partition, shouldn't drop NOT NULL if parent has the same */
+   /*
+    * If rel is partition, shouldn't drop NOT NULL if parent has the same.
+    */
    if (rel->rd_rel->relispartition)
    {
        Oid         parentId = get_partition_parent(RelationGetRelid(rel), false);
    }
 
    /*
-    * Okay, actually perform the catalog change ... if needed
+    * Find the constraint that makes this column NOT NULL.
     */
-   if (attTup->attnotnull)
+   conTup = findNotNullConstraint(RelationGetRelid(rel), colName);
+   if (conTup == NULL)
    {
-       attTup->attnotnull = false;
+       Bitmapset  *pkcols;
 
-       CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+       /*
+        * There's no not-null constraint, so throw an error.  If the column
+        * is in a primary key, we can throw a specific error.  Otherwise,
+        * this is unexpected.
+        */
+       pkcols = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY);
+       if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber,
+                         pkcols))
+           ereport(ERROR,
+                   errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                   errmsg("column \"%s\" is in a primary key", colName));
 
-       ObjectAddressSubSet(address, RelationRelationId,
-                           RelationGetRelid(rel), attnum);
+       /* this shouldn't happen */
+       elog(ERROR, "could not find not-null constraint on column \"%s\", relation \"%s\"",
+            colName, RelationGetRelationName(rel));
    }
-   else
-       address = InvalidObjectAddress;
+
+   readyRels = NIL;
+   dropconstraint_internal(rel, conTup, DROP_RESTRICT, recurse, false,
+                           false, &readyRels, lockmode);
+
+   heap_freetuple(conTup);
 
    InvokeObjectPostAlterHook(RelationRelationId,
                              RelationGetRelid(rel), attnum);
 }
 
 /*
- * ALTER TABLE ALTER COLUMN SET NOT NULL
+ * Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3
+ * to verify it; recurses to apply the same to children.
+ *
+ * When called to alter an existing table, 'wqueue' must be given so that we can
+ * queue a check that existing tuples pass the constraint.  When called from
+ * table creation, 'wqueue' should be passed as NULL.
+ *
+ * Returns true if the flag was set in any table, otherwise false.
  */
-
-static void
-ATPrepSetNotNull(List **wqueue, Relation rel,
-                AlterTableCmd *cmd, bool recurse, bool recursing,
-                LOCKMODE lockmode, AlterTableUtilityContext *context)
+static bool
+set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, bool recurse,
+              LOCKMODE lockmode)
 {
-   /*
-    * If we're already recursing, there's nothing to do; the topmost
-    * invocation of ATSimpleRecursion already visited all children.
-    */
-   if (recursing)
-       return;
+   HeapTuple   tuple;
+   Form_pg_attribute attForm;
+   bool        retval = false;
 
-   /*
-    * If the target column is already marked NOT NULL, we can skip recursing
-    * to children, because their columns should already be marked NOT NULL as
-    * well.  But there's no point in checking here unless the relation has
-    * some children; else we can just wait till execution to check.  (If it
-    * does have children, however, this can save taking per-child locks
-    * unnecessarily.  This greatly improves concurrency in some parallel
-    * restore scenarios.)
-    *
-    * Unfortunately, we can only apply this optimization to partitioned
-    * tables, because traditional inheritance doesn't enforce that child
-    * columns be NOT NULL when their parent is.  (That's a bug that should
-    * get fixed someday.)
-    */
-   if (rel->rd_rel->relhassubclass &&
-       rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+   tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
+   if (!HeapTupleIsValid(tuple))
+       elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+            attnum, RelationGetRelid(rel));
+   attForm = (Form_pg_attribute) GETSTRUCT(tuple);
+   if (!attForm->attnotnull)
    {
-       HeapTuple   tuple;
-       bool        attnotnull;
+       Relation    attr_rel;
 
-       tuple = SearchSysCacheAttName(RelationGetRelid(rel), cmd->name);
+       attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
 
-       /* Might as well throw the error now, if name is bad */
-       if (!HeapTupleIsValid(tuple))
-           ereport(ERROR,
-                   (errcode(ERRCODE_UNDEFINED_COLUMN),
-                    errmsg("column \"%s\" of relation \"%s\" does not exist",
-                           cmd->name, RelationGetRelationName(rel))));
+       attForm->attnotnull = true;
+       CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
 
-       attnotnull = ((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull;
-       ReleaseSysCache(tuple);
-       if (attnotnull)
-           return;
+       table_close(attr_rel, RowExclusiveLock);
+
+       /*
+        * And set up for existing values to be checked, unless another
+        * constraint already proves this.
+        */
+       if (wqueue && !NotNullImpliedByRelConstraints(rel, attForm))
+       {
+           AlteredTableInfo *tab;
+
+           tab = ATGetQueueEntry(wqueue, rel);
+           tab->verify_new_notnull = true;
+       }
+
+       retval = true;
    }
 
-   /*
-    * If we have ALTER TABLE ONLY ... SET NOT NULL on a partitioned table,
-    * apply ALTER TABLE ... CHECK NOT NULL to every child.  Otherwise, use
-    * normal recursion logic.
-    */
-   if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
-       !recurse)
+   if (recurse)
    {
-       AlterTableCmd *newcmd = makeNode(AlterTableCmd);
+       List       *children;
+       ListCell   *lc;
+
+       children = find_inheritance_children(RelationGetRelid(rel), lockmode);
+       foreach(lc, children)
+       {
+           Oid         childrelid = lfirst_oid(lc);
+           Relation    childrel;
+           AttrNumber  childattno;
+
+           /* find_inheritance_children already got lock */
+           childrel = table_open(childrelid, NoLock);
+           CheckTableNotInUse(childrel, "ALTER TABLE");
 
-       newcmd->subtype = AT_CheckNotNull;
-       newcmd->name = pstrdup(cmd->name);
-       ATSimpleRecursion(wqueue, rel, newcmd, true, lockmode, context);
+           childattno = get_attnum(RelationGetRelid(childrel),
+                                   get_attname(RelationGetRelid(rel), attnum,
+                                               false));
+           retval |= set_attnotnull(wqueue, childrel, childattno,
+                                    recurse, lockmode);
+           table_close(childrel, NoLock);
+       }
    }
-   else
-       ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
+
+   return retval;
 }
 
 /*
- * Return the address of the modified column.  If the column was already NOT
- * NULL, InvalidObjectAddress is returned.
+ * ALTER TABLE ALTER COLUMN SET NOT NULL
+ *
+ * Add a not-null constraint to a single table and its children.  Returns
+ * the address of the constraint added to the parent relation, if one gets
+ * added, or InvalidObjectAddress otherwise.
+ *
+ * We must recurse to child tables during execution, rather than using
+ * ALTER TABLE's normal prep-time recursion.
  */
 static ObjectAddress
-ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
-                const char *colName, LOCKMODE lockmode)
+ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
+                bool recurse, bool recursing, List **readyRels,
+                LOCKMODE lockmode)
 {
    HeapTuple   tuple;
+   Relation    constr_rel;
+   ScanKeyData skey;
+   SysScanDesc conscan;
    AttrNumber  attnum;
-   Relation    attr_rel;
    ObjectAddress address;
+   Constraint *constraint;
+   CookedConstraint *ccon;
+   List       *cooked;
+   bool        is_no_inherit = false;
+   List       *ready = NIL;
 
    /*
-    * lookup the attribute
+    * In cases of multiple inheritance, we might visit the same child more
+    * than once.  In the topmost call, set up a list that we fill with all
+    * visited relations, to skip those.
     */
-   attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
+   if (readyRels == NULL)
+   {
+       Assert(!recursing);
+       readyRels = &ready;
+   }
+   if (list_member_oid(*readyRels, RelationGetRelid(rel)))
+       return InvalidObjectAddress;
+   *readyRels = lappend_oid(*readyRels, RelationGetRelid(rel));
 
-   tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
+   /* At top level, permission check was done in ATPrepCmd, else do it */
+   if (recursing)
+   {
+       ATSimplePermissions(AT_AddConstraint, rel, ATT_TABLE | ATT_FOREIGN_TABLE);
+       Assert(conName != NULL);
+   }
 
-   if (!HeapTupleIsValid(tuple))
+   attnum = get_attnum(RelationGetRelid(rel), colName);
+   if (attnum == InvalidAttrNumber)
        ereport(ERROR,
                (errcode(ERRCODE_UNDEFINED_COLUMN),
                 errmsg("column \"%s\" of relation \"%s\" does not exist",
                        colName, RelationGetRelationName(rel))));
 
-   attnum = ((Form_pg_attribute) GETSTRUCT(tuple))->attnum;
-
    /* Prevent them from altering a system attribute */
    if (attnum <= 0)
        ereport(ERROR,
                 errmsg("cannot alter system column \"%s\"",
                        colName)));
 
-   /*
-    * Okay, actually perform the catalog change ... if needed
-    */
-   if (!((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull)
+   /* See if there's already a constraint */
+   constr_rel = table_open(ConstraintRelationId, RowExclusiveLock);
+   ScanKeyInit(&skey,
+               Anum_pg_constraint_conrelid,
+               BTEqualStrategyNumber, F_OIDEQ,
+               ObjectIdGetDatum(RelationGetRelid(rel)));
+   conscan = systable_beginscan(constr_rel, ConstraintRelidTypidNameIndexId, true,
+                                NULL, 1, &skey);
+
+   while (HeapTupleIsValid(tuple = systable_getnext(conscan)))
    {
-       ((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull = true;
+       Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+       bool        changed = false;
+       HeapTuple   copytup;
 
-       CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+       if (conForm->contype != CONSTRAINT_NOTNULL)
+           continue;
+
+       if (extractNotNullColumn(tuple) != attnum)
+           continue;
+
+       copytup = heap_copytuple(tuple);
+       conForm = (Form_pg_constraint) GETSTRUCT(copytup);
 
        /*
-        * Ordinarily phase 3 must ensure that no NULLs exist in columns that
-        * are set NOT NULL; however, if we can find a constraint which proves
-        * this then we can skip that.  We needn't bother looking if we've
-        * already found that we must verify some other NOT NULL constraint.
+        * If we find an appropriate constraint, we're almost done, but just
+        * need to change some properties on it: if we're recursing, increment
+        * coninhcount; if not, set conislocal if not already set.
         */
-       if (!tab->verify_new_notnull &&
-           !NotNullImpliedByRelConstraints(rel, (Form_pg_attribute) GETSTRUCT(tuple)))
+       if (recursing)
        {
-           /* Tell Phase 3 it needs to test the constraint */
-           tab->verify_new_notnull = true;
+           conForm->coninhcount++;
+           changed = true;
+       }
+       else if (!conForm->conislocal)
+       {
+           conForm->conislocal = true;
+           changed = true;
        }
 
-       ObjectAddressSubSet(address, RelationRelationId,
-                           RelationGetRelid(rel), attnum);
+       if (changed)
+       {
+           CatalogTupleUpdate(constr_rel, ©tup->t_self, copytup);
+           ObjectAddressSet(address, ConstraintRelationId, conForm->oid);
+       }
+
+       systable_endscan(conscan);
+       table_close(constr_rel, RowExclusiveLock);
+
+       if (changed)
+           return address;
+       else
+           return InvalidObjectAddress;
+   }
+
+   systable_endscan(conscan);
+   table_close(constr_rel, RowExclusiveLock);
+
+   /*
+    * If we're asked not to recurse, and children exist, raise an error for
+    * partitioned tables.  For inheritance, we act as if NO INHERIT had been
+    * specified.
+    */
+   if (!recurse &&
+       find_inheritance_children(RelationGetRelid(rel),
+                                 NoLock) != NIL)
+   {
+       if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+           ereport(ERROR,
+                   errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                   errmsg("constraint must be added to child tables too"),
+                   errhint("Do not specify the ONLY keyword."));
+       else
+           is_no_inherit = true;
    }
-   else
-       address = InvalidObjectAddress;
+
+   /*
+    * No constraint exists; we must add one.  First determine a name to use,
+    * if we haven't already.
+    */
+   if (!recursing)
+   {
+       Assert(conName == NULL);
+       conName = ChooseConstraintName(RelationGetRelationName(rel),
+                                      colName, "not_null",
+                                      RelationGetNamespace(rel),
+                                      NIL);
+   }
+   constraint = makeNode(Constraint);
+   constraint->contype = CONSTR_NOTNULL;
+   constraint->conname = conName;
+   constraint->deferrable = false;
+   constraint->initdeferred = false;
+   constraint->location = -1;
+   constraint->keys = list_make1(makeString(colName));
+   constraint->is_no_inherit = is_no_inherit;
+   constraint->inhcount = recursing ? 1 : 0;
+   constraint->skip_validation = false;
+   constraint->initially_valid = true;
+
+   /* and do it */
+   cooked = AddRelationNewConstraints(rel, NIL, list_make1(constraint),
+                                      false, !recursing, false, NULL);
+   ccon = linitial(cooked);
+   ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
 
    InvokeObjectPostAlterHook(RelationRelationId,
                              RelationGetRelid(rel), attnum);
 
-   table_close(attr_rel, RowExclusiveLock);
+   /*
+    * Mark pg_attribute.attnotnull for the column. Tell that function not to
+    * recurse, because we're going to do it here.
+    */
+   set_attnotnull(wqueue, rel, attnum, false, lockmode);
+
+   /*
+    * Recurse to propagate the constraint to children that don't have one.
+    */
+   if (recurse)
+   {
+       List       *children;
+       ListCell   *lc;
+
+       children = find_inheritance_children(RelationGetRelid(rel),
+                                            lockmode);
+
+       foreach(lc, children)
+       {
+           Relation    childrel;
+
+           childrel = table_open(lfirst_oid(lc), NoLock);
+
+           ATExecSetNotNull(wqueue, childrel,
+                            conName, colName, recurse, true,
+                            readyRels, lockmode);
+
+           table_close(childrel, NoLock);
+       }
+   }
 
    return address;
 }
 
 /*
- * ALTER TABLE ALTER COLUMN CHECK NOT NULL
+ * ALTER TABLE ALTER COLUMN SET ATTNOTNULL
  *
- * This doesn't exist in the grammar, but we generate AT_CheckNotNull
- * commands against the partitions of a partitioned table if the user
- * writes ALTER TABLE ONLY ... SET NOT NULL on the partitioned table,
- * or tries to create a primary key on it (which internally creates
- * AT_SetNotNull on the partitioned table).   Such a command doesn't
- * allow us to actually modify any partition, but we want to let it
- * go through if the partitions are already properly marked.
- *
- * In future, this might need to adjust the child table's state, likely
- * by incrementing an inheritance count for the attnotnull constraint.
- * For now we need only check for the presence of the flag.
+ * This doesn't exist in the grammar; it's used when creating a
+ * primary key and the column is not already marked attnotnull.
  */
-static void
-ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel,
-                  const char *colName, LOCKMODE lockmode)
+static ObjectAddress
+ATExecSetAttNotNull(List **wqueue, Relation rel,
+                   const char *colName, LOCKMODE lockmode)
 {
-   HeapTuple   tuple;
-
-   tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
+   AttrNumber  attnum;
+   ObjectAddress address = InvalidObjectAddress;
 
-   if (!HeapTupleIsValid(tuple))
+   attnum = get_attnum(RelationGetRelid(rel), colName);
+   if (attnum == InvalidAttrNumber)
        ereport(ERROR,
-               (errcode(ERRCODE_UNDEFINED_COLUMN),
-                errmsg("column \"%s\" of relation \"%s\" does not exist",
-                       colName, RelationGetRelationName(rel))));
+               errcode(ERRCODE_UNDEFINED_COLUMN),
+               errmsg("column \"%s\" of relation \"%s\" does not exist",
+                      colName, RelationGetRelationName(rel)));
 
-   if (!((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull)
-       ereport(ERROR,
-               (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-                errmsg("constraint must be added to child tables too"),
-                errdetail("Column \"%s\" of relation \"%s\" is not already NOT NULL.",
-                          colName, RelationGetRelationName(rel)),
-                errhint("Do not specify the ONLY keyword.")));
+   /*
+    * Make the change, if necessary, and only if so report the column as
+    * changed
+    */
+   if (set_attnotnull(wqueue, rel, attnum, false, lockmode))
+       ObjectAddressSubSet(address, RelationRelationId,
+                           RelationGetRelid(rel), attnum);
 
-   ReleaseSysCache(tuple);
+   return address;
 }
 
 /*
        performMultipleDeletions(addrs, behavior, 0);
        free_object_addresses(addrs);
    }
-
-   return object;
+
+   return object;
+}
+
+/*
+ * Prepare to add a primary key on an inheritance parent, by adding NOT NULL
+ * constraint on its children.
+ */
+static void
+ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
+                   LOCKMODE lockmode, AlterTableUtilityContext *context)
+{
+   List       *children;
+   List       *newconstrs = NIL;
+   ListCell   *lc;
+   IndexStmt  *stmt;
+
+   /* No work if no legacy inheritance children are present */
+   if (rel->rd_rel->relkind != RELKIND_RELATION ||
+       !rel->rd_rel->relhassubclass)
+       return;
+
+   children = find_inheritance_children(RelationGetRelid(rel), lockmode);
+
+   stmt = castNode(IndexStmt, cmd->def);
+   foreach(lc, stmt->indexParams)
+   {
+       IndexElem  *elem = lfirst_node(IndexElem, lc);
+       Constraint *nnconstr;
+
+       Assert(elem->expr == NULL);
+
+       nnconstr = makeNode(Constraint);
+       nnconstr->contype = CONSTR_NOTNULL;
+       nnconstr->conname = NULL;   /* XXX use PK name? */
+       nnconstr->inhcount = 1;
+       nnconstr->deferrable = false;
+       nnconstr->initdeferred = false;
+       nnconstr->location = -1;
+       nnconstr->keys = list_make1(makeString(elem->name));
+       nnconstr->skip_validation = false;
+       nnconstr->initially_valid = true;
+
+       newconstrs = lappend(newconstrs, nnconstr);
+   }
+
+   foreach(lc, children)
+   {
+       Oid         childrelid = lfirst_oid(lc);
+       Relation    childrel = table_open(childrelid, NoLock);
+       AlterTableCmd *newcmd = makeNode(AlterTableCmd);
+       ListCell   *lc2;
+
+       newcmd->subtype = AT_AddConstraint;
+       newcmd->recurse = true;
+
+       foreach(lc2, newconstrs)
+       {
+           /* ATPrepCmd copies newcmd, so we can scribble on it here */
+           newcmd->def = lfirst(lc2);
+
+           ATPrepCmd(wqueue, childrel, newcmd,
+                     true, false, lockmode, context);
+       }
+
+       table_close(childrel, NoLock);
+   }
 }
 
 /*
    Assert(IsA(newConstraint, Constraint));
 
    /*
-    * Currently, we only expect to see CONSTR_CHECK and CONSTR_FOREIGN nodes
-    * arriving here (see the preprocessing done in parse_utilcmd.c).  Use a
-    * switch anyway to make it easier to add more code later.
+    * Currently, we only expect to see CONSTR_CHECK, CONSTR_NOTNULL and
+    * CONSTR_FOREIGN nodes arriving here (see the preprocessing done in
+    * parse_utilcmd.c).
     */
    switch (newConstraint->contype)
    {
        case CONSTR_CHECK:
+       case CONSTR_NOTNULL:
            address =
-               ATAddCheckConstraint(wqueue, tab, rel,
-                                    newConstraint, recurse, false, is_readd,
-                                    lockmode);
+               ATAddCheckNNConstraint(wqueue, tab, rel,
+                                      newConstraint, recurse, false, is_readd,
+                                      lockmode);
            break;
 
        case CONSTR_FOREIGN:
 }
 
 /*
- * Add a check constraint to a single table and its children.  Returns the
- * address of the constraint added to the parent relation, if one gets added,
- * or InvalidObjectAddress otherwise.
+ * Add a check or not-null constraint to a single table and its children.
+ * Returns the address of the constraint added to the parent relation,
+ * if one gets added, or InvalidObjectAddress otherwise.
  *
  * Subroutine for ATExecAddConstraint.
  *
  * the parent table and pass that down.
  */
 static ObjectAddress
-ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
-                    Constraint *constr, bool recurse, bool recursing,
-                    bool is_readd, LOCKMODE lockmode)
+ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
+                      Constraint *constr, bool recurse, bool recursing,
+                      bool is_readd, LOCKMODE lockmode)
 {
    List       *newcons;
    ListCell   *lcon;
    {
        CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
 
-       if (!ccon->skip_validation)
+       if (!ccon->skip_validation && ccon->contype != CONSTR_NOTNULL)
        {
            NewConstraint *newcon;
 
        if (constr->conname == NULL)
            constr->conname = ccon->name;
 
+       /*
+        * If adding a not-null constraint, set the pg_attribute flag and tell
+        * phase 3 to verify existing rows, if needed.
+        */
+       if (constr->contype == CONSTR_NOTNULL)
+           set_attnotnull(wqueue, rel, ccon->attnum,
+                          !ccon->is_no_inherit, lockmode);
+
        ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
    }
 
    /* At this point we must have a locked-down name to use */
-   Assert(constr->conname != NULL);
+   Assert(newcons == NIL || constr->conname != NULL);
 
    /* Advance command counter in case same table is visited multiple times */
    CommandCounterIncrement();
                (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
                 errmsg("constraint must be added to child tables too")));
 
+   /*
+    * The constraint must appear as inherited in children, so create a
+    * modified constraint object to use.
+    */
+   constr = copyObject(constr);
+   constr->inhcount = 1;
    foreach(child, children)
    {
        Oid         childrelid = lfirst_oid(child);
        /* Find or create work queue entry for this table */
        childtab = ATGetQueueEntry(wqueue, childrel);
 
-       /* Recurse to child */
-       ATAddCheckConstraint(wqueue, childtab, childrel,
-                            constr, recurse, true, is_readd, lockmode);
+       /*
+        * Recurse to child.  XXX if we didn't create a constraint on the
+        * parent because it already existed, and we do create one on a child,
+        * should we return that child's constraint ObjectAddress here?
+        */
+       ATAddCheckNNConstraint(wqueue, childtab, childrel,
+                              constr, recurse, true, is_readd, lockmode);
 
        table_close(childrel, NoLock);
    }
                     bool recurse, bool recursing,
                     bool missing_ok, LOCKMODE lockmode)
 {
-   List       *children;
-   ListCell   *child;
    Relation    conrel;
-   Form_pg_constraint con;
    SysScanDesc scan;
    ScanKeyData skey[3];
    HeapTuple   tuple;
    bool        found = false;
-   bool        is_no_inherit_constraint = false;
-   char        contype;
 
    /* At top level, permission check was done in ATPrepCmd, else do it */
    if (recursing)
    /* There can be at most one matching row */
    if (HeapTupleIsValid(tuple = systable_getnext(scan)))
    {
-       ObjectAddress conobj;
+       List       *readyRels = NIL;
+
+       dropconstraint_internal(rel, tuple, behavior, recurse, recursing,
+                               missing_ok, &readyRels, lockmode);
+       found = true;
+   }
 
-       con = (Form_pg_constraint) GETSTRUCT(tuple);
+   systable_endscan(scan);
 
-       /* Don't drop inherited constraints */
-       if (con->coninhcount > 0 && !recursing)
+   if (!found)
+   {
+       if (!missing_ok)
            ereport(ERROR,
-                   (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-                    errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"",
-                           constrName, RelationGetRelationName(rel))));
+                   errcode(ERRCODE_UNDEFINED_OBJECT),
+                   errmsg("constraint \"%s\" of relation \"%s\" does not exist",
+                          constrName, RelationGetRelationName(rel)));
+       else
+           ereport(NOTICE,
+                   errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping",
+                          constrName, RelationGetRelationName(rel)));
+   }
 
-       is_no_inherit_constraint = con->connoinherit;
-       contype = con->contype;
+   table_close(conrel, RowExclusiveLock);
+}
 
-       /*
-        * If it's a foreign-key constraint, we'd better lock the referenced
-        * table and check that that's not in use, just as we've already done
-        * for the constrained table (else we might, eg, be dropping a trigger
-        * that has unfired events).  But we can/must skip that in the
-        * self-referential case.
-        */
-       if (contype == CONSTRAINT_FOREIGN &&
-           con->confrelid != RelationGetRelid(rel))
-       {
-           Relation    frel;
+/*
+ * Remove a constraint, using its pg_constraint tuple
+ *
+ * Implementation for ALTER TABLE DROP CONSTRAINT and ALTER TABLE ALTER COLUMN
+ * DROP NOT NULL.
+ *
+ * Returns the address of the constraint being removed.
+ */
+static ObjectAddress
+dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior behavior,
+                       bool recurse, bool recursing, bool missing_ok, List **readyRels,
+                       LOCKMODE lockmode)
+{
+   Relation    conrel;
+   Form_pg_constraint con;
+   ObjectAddress conobj;
+   List       *children;
+   ListCell   *child;
+   bool        is_no_inherit_constraint = false;
+   bool        dropping_pk = false;
+   char       *constrName;
+   List       *unconstrained_cols = NIL;
+   char       *colname;
 
-           /* Must match lock taken by RemoveTriggerById: */
-           frel = table_open(con->confrelid, AccessExclusiveLock);
-           CheckTableNotInUse(frel, "ALTER TABLE");
-           table_close(frel, NoLock);
-       }
+   if (list_member_oid(*readyRels, RelationGetRelid(rel)))
+       return InvalidObjectAddress;
+   *readyRels = lappend_oid(*readyRels, RelationGetRelid(rel));
 
-       /*
-        * Perform the actual constraint deletion
-        */
-       conobj.classId = ConstraintRelationId;
-       conobj.objectId = con->oid;
-       conobj.objectSubId = 0;
+   conrel = table_open(ConstraintRelationId, RowExclusiveLock);
 
-       performDeletion(&conobj, behavior, 0);
+   con = (Form_pg_constraint) GETSTRUCT(constraintTup);
+   constrName = NameStr(con->conname);
 
-       found = true;
+   /* Don't allow drop of inherited constraints */
+   if (con->coninhcount > 0 && !recursing)
+       ereport(ERROR,
+               (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                errmsg("cannot drop inherited constraint \"%s\" of relation \"%s\"",
+                       constrName, RelationGetRelationName(rel))));
+
+   /*
+    * See if we have a not-null constraint or a PRIMARY KEY.  If so, we have
+    * more checks and actions below, so obtain the list of columns that are
+    * constrained by the constraint being dropped.
+    */
+   if (con->contype == CONSTRAINT_NOTNULL)
+   {
+       AttrNumber  colnum = extractNotNullColumn(constraintTup);
+
+       if (colnum != InvalidAttrNumber)
+           unconstrained_cols = list_make1_int(colnum);
    }
+   else if (con->contype == CONSTRAINT_PRIMARY)
+   {
+       Datum       adatum;
+       ArrayType  *arr;
+       int         numkeys;
+       bool        isNull;
+       int16      *attnums;
 
-   systable_endscan(scan);
+       dropping_pk = true;
 
-   if (!found)
+       adatum = heap_getattr(constraintTup, Anum_pg_constraint_conkey,
+                             RelationGetDescr(conrel), &isNull);
+       if (isNull)
+           elog(ERROR, "null conkey for constraint %u", con->oid);
+       arr = DatumGetArrayTypeP(adatum);   /* ensure not toasted */
+       numkeys = ARR_DIMS(arr)[0];
+       if (ARR_NDIM(arr) != 1 ||
+           numkeys < 0 ||
+           ARR_HASNULL(arr) ||
+           ARR_ELEMTYPE(arr) != INT2OID)
+           elog(ERROR, "conkey is not a 1-D smallint array");
+       attnums = (int16 *) ARR_DATA_PTR(arr);
+
+       for (int i = 0; i < numkeys; i++)
+           unconstrained_cols = lappend_int(unconstrained_cols, attnums[i]);
+   }
+
+   is_no_inherit_constraint = con->connoinherit;
+
+   /*
+    * If it's a foreign-key constraint, we'd better lock the referenced table
+    * and check that that's not in use, just as we've already done for the
+    * constrained table (else we might, eg, be dropping a trigger that has
+    * unfired events).  But we can/must skip that in the self-referential
+    * case.
+    */
+   if (con->contype == CONSTRAINT_FOREIGN &&
+       con->confrelid != RelationGetRelid(rel))
    {
-       if (!missing_ok)
-       {
-           ereport(ERROR,
-                   (errcode(ERRCODE_UNDEFINED_OBJECT),
-                    errmsg("constraint \"%s\" of relation \"%s\" does not exist",
-                           constrName, RelationGetRelationName(rel))));
-       }
-       else
+       Relation    frel;
+
+       /* Must match lock taken by RemoveTriggerById: */
+       frel = table_open(con->confrelid, AccessExclusiveLock);
+       CheckTableNotInUse(frel, "ALTER TABLE");
+       table_close(frel, NoLock);
+   }
+
+   /*
+    * Perform the actual constraint deletion
+    */
+   ObjectAddressSet(conobj, ConstraintRelationId, con->oid);
+   performDeletion(&conobj, behavior, 0);
+
+   /*
+    * If this was a NOT NULL or the primary key, the constrained columns must
+    * have had pg_attribute.attnotnull set.  See if we need to reset it, and
+    * do so.
+    */
+   if (unconstrained_cols)
+   {
+       Relation    attrel;
+       Bitmapset  *pkcols;
+       Bitmapset  *ircols;
+       ListCell   *lc;
+
+       /* Make the above deletion visible */
+       CommandCounterIncrement();
+
+       attrel = table_open(AttributeRelationId, RowExclusiveLock);
+
+       /*
+        * We want to test columns for their presence in the primary key, but
+        * only if we're not dropping it.
+        */
+       pkcols = dropping_pk ? NULL :
+           RelationGetIndexAttrBitmap(rel,
+                                      INDEX_ATTR_BITMAP_PRIMARY_KEY);
+       ircols = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY);
+
+       foreach(lc, unconstrained_cols)
        {
-           ereport(NOTICE,
-                   (errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping",
-                           constrName, RelationGetRelationName(rel))));
-           table_close(conrel, RowExclusiveLock);
-           return;
+           AttrNumber  attnum = lfirst_int(lc);
+           HeapTuple   atttup;
+           HeapTuple   contup;
+           Form_pg_attribute attForm;
+
+           /*
+            * Obtain pg_attribute tuple and verify conditions on it.  We use
+            * a copy we can scribble on.
+            */
+           atttup = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
+           if (!HeapTupleIsValid(atttup))
+               elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+                    attnum, RelationGetRelid(rel));
+           attForm = (Form_pg_attribute) GETSTRUCT(atttup);
+
+           /*
+            * Since the above deletion has been made visible, we can now
+            * search for any remaining constraints on this column (or these
+            * columns, in the case we're dropping a multicol primary key.)
+            * Then, verify whether any further NOT NULL or primary key
+            * exists, and reset attnotnull if none.
+            *
+            * However, if this is a generated identity column, abort the
+            * whole thing with a specific error message, because the
+            * constraint is required in that case.
+            */
+           contup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+           if (contup ||
+               bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber,
+                             pkcols))
+               continue;
+
+           /*
+            * It's not valid to drop the not-null constraint for a GENERATED
+            * AS IDENTITY column.
+            */
+           if (attForm->attidentity)
+               ereport(ERROR,
+                       errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                       errmsg("column \"%s\" of relation \"%s\" is an identity column",
+                              get_attname(RelationGetRelid(rel), attnum,
+                                          false),
+                              RelationGetRelationName(rel)));
+
+           /*
+            * It's not valid to drop the not-null constraint for a column in
+            * the replica identity index, either. (FULL is not affected.)
+            */
+           if (bms_is_member(lfirst_int(lc) - FirstLowInvalidHeapAttributeNumber, ircols))
+               ereport(ERROR,
+                       errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                       errmsg("column \"%s\" is in index used as replica identity",
+                              get_attname(RelationGetRelid(rel), lfirst_int(lc), false)));
+
+           /* Reset attnotnull */
+           if (attForm->attnotnull)
+           {
+               attForm->attnotnull = false;
+               CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
+           }
        }
+       table_close(attrel, RowExclusiveLock);
    }
 
    /*
-    * For partitioned tables, non-CHECK inherited constraints are dropped via
-    * the dependency mechanism, so we're done here.
+    * For partitioned tables, non-CHECK, non-NOT-NULL inherited constraints
+    * are dropped via the dependency mechanism, so we're done here.
     */
-   if (contype != CONSTRAINT_CHECK &&
+   if (con->contype != CONSTRAINT_CHECK &&
+       con->contype != CONSTRAINT_NOTNULL &&
        rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
    {
        table_close(conrel, RowExclusiveLock);
-       return;
+       return conobj;
    }
 
    /*
                 errmsg("cannot remove constraint from only the partitioned table when partitions exist"),
                 errhint("Do not specify the ONLY keyword.")));
 
+   /* For not-null constraints we recurse by column name */
+   if (con->contype == CONSTRAINT_NOTNULL)
+       colname = NameStr(TupleDescAttr(RelationGetDescr(rel),
+                                       linitial_int(unconstrained_cols) - 1)->attname);
+   else
+       colname = NULL;         /* keep compiler quiet */
+
    foreach(child, children)
    {
        Oid         childrelid = lfirst_oid(child);
        Relation    childrel;
-       HeapTuple   copy_tuple;
+       HeapTuple   tuple;
+       Form_pg_constraint childcon;
+
+       if (list_member_oid(*readyRels, childrelid))
+           continue;           /* child already processed */
 
        /* find_inheritance_children already got lock */
        childrel = table_open(childrelid, NoLock);
        CheckTableNotInUse(childrel, "ALTER TABLE");
 
-       ScanKeyInit(&skey[0],
-                   Anum_pg_constraint_conrelid,
-                   BTEqualStrategyNumber, F_OIDEQ,
-                   ObjectIdGetDatum(childrelid));
-       ScanKeyInit(&skey[1],
-                   Anum_pg_constraint_contypid,
-                   BTEqualStrategyNumber, F_OIDEQ,
-                   ObjectIdGetDatum(InvalidOid));
-       ScanKeyInit(&skey[2],
-                   Anum_pg_constraint_conname,
-                   BTEqualStrategyNumber, F_NAMEEQ,
-                   CStringGetDatum(constrName));
-       scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId,
-                                 true, NULL, 3, skey);
-
-       /* There can be at most one matching row */
-       if (!HeapTupleIsValid(tuple = systable_getnext(scan)))
-           ereport(ERROR,
-                   (errcode(ERRCODE_UNDEFINED_OBJECT),
-                    errmsg("constraint \"%s\" of relation \"%s\" does not exist",
-                           constrName,
-                           RelationGetRelationName(childrel))));
-
-       copy_tuple = heap_copytuple(tuple);
-
-       systable_endscan(scan);
+       /*
+        * We search for not-null constraint by column number, and other
+        * constraints by name.
+        */
+       if (con->contype == CONSTRAINT_NOTNULL)
+       {
+           tuple = findNotNullConstraint(childrelid, colname);
+           if (!HeapTupleIsValid(tuple))
+               elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
+                    colname, RelationGetRelid(childrel));
+       }
+       else
+       {
+           SysScanDesc scan;
+           ScanKeyData skey[3];
+
+           ScanKeyInit(&skey[0],
+                       Anum_pg_constraint_conrelid,
+                       BTEqualStrategyNumber, F_OIDEQ,
+                       ObjectIdGetDatum(childrelid));
+           ScanKeyInit(&skey[1],
+                       Anum_pg_constraint_contypid,
+                       BTEqualStrategyNumber, F_OIDEQ,
+                       ObjectIdGetDatum(InvalidOid));
+           ScanKeyInit(&skey[2],
+                       Anum_pg_constraint_conname,
+                       BTEqualStrategyNumber, F_NAMEEQ,
+                       CStringGetDatum(constrName));
+           scan = systable_beginscan(conrel, ConstraintRelidTypidNameIndexId,
+                                     true, NULL, 3, skey);
+           /* There can only be one, so no need to loop */
+           tuple = systable_getnext(scan);
+           if (!HeapTupleIsValid(tuple))
+               ereport(ERROR,
+                       (errcode(ERRCODE_UNDEFINED_OBJECT),
+                        errmsg("constraint \"%s\" of relation \"%s\" does not exist",
+                               constrName,
+                               RelationGetRelationName(childrel))));
+           tuple = heap_copytuple(tuple);
+           systable_endscan(scan);
+       }
 
-       con = (Form_pg_constraint) GETSTRUCT(copy_tuple);
+       childcon = (Form_pg_constraint) GETSTRUCT(tuple);
 
-       /* Right now only CHECK constraints can be inherited */
-       if (con->contype != CONSTRAINT_CHECK)
-           elog(ERROR, "inherited constraint is not a CHECK constraint");
+       /* Right now only CHECK and not-null constraints can be inherited */
+       if (childcon->contype != CONSTRAINT_CHECK &&
+           childcon->contype != CONSTRAINT_NOTNULL)
+           elog(ERROR, "inherited constraint is not a CHECK or not-null constraint");
 
-       if (con->coninhcount <= 0)  /* shouldn't happen */
+       if (childcon->coninhcount <= 0) /* shouldn't happen */
            elog(ERROR, "relation %u has non-inherited constraint \"%s\"",
-                childrelid, constrName);
+                childrelid, NameStr(childcon->conname));
 
        if (recurse)
        {
             * If the child constraint has other definition sources, just
             * decrement its inheritance count; if not, recurse to delete it.
             */
-           if (con->coninhcount == 1 && !con->conislocal)
+           if (childcon->coninhcount == 1 && !childcon->conislocal)
            {
                /* Time to delete this child constraint, too */
-               ATExecDropConstraint(childrel, constrName, behavior,
-                                    true, true,
-                                    false, lockmode);
+               dropconstraint_internal(childrel, tuple, behavior,
+                                       recurse, true, missing_ok, readyRels,
+                                       lockmode);
            }
            else
            {
                /* Child constraint must survive my deletion */
-               con->coninhcount--;
-               CatalogTupleUpdate(conrel, ©_tuple->t_self, copy_tuple);
+               childcon->coninhcount--;
+               CatalogTupleUpdate(conrel, &tuple->t_self, tuple);
 
                /* Make update visible */
                CommandCounterIncrement();
        else
        {
            /*
-            * If we were told to drop ONLY in this table (no recursion), we
-            * need to mark the inheritors' constraints as locally defined
-            * rather than inherited.
+            * If we were told to drop ONLY in this table (no recursion) and
+            * there are no further parents for this constraint, we need to
+            * mark the inheritors' constraints as locally defined rather than
+            * inherited.
             */
-           con->coninhcount--;
-           con->conislocal = true;
+           childcon->coninhcount--;
+           if (childcon->coninhcount == 0)
+               childcon->conislocal = true;
 
-           CatalogTupleUpdate(conrel, ©_tuple->t_self, copy_tuple);
+           CatalogTupleUpdate(conrel, &tuple->t_self, tuple);
 
            /* Make update visible */
            CommandCounterIncrement();
        }
 
-       heap_freetuple(copy_tuple);
+       heap_freetuple(tuple);
 
        table_close(childrel, NoLock);
    }
 
+   /*
+    * In addition, when dropping a primary key from a legacy-inheritance
+    * parent table, we must recurse to children to mark the corresponding NOT
+    * NULL constraint as no longer inherited, or drop it if this its last
+    * reference.
+    */
+   if (con->contype == CONSTRAINT_PRIMARY &&
+       rel->rd_rel->relkind == RELKIND_RELATION &&
+       rel->rd_rel->relhassubclass)
+   {
+       List       *colnames = NIL;
+       ListCell   *lc;
+       List       *pkready = NIL;
+
+       /*
+        * Because primary keys are always marked as NO INHERIT, we don't have
+        * a list of children yet, so obtain one now.
+        */
+       children = find_inheritance_children(RelationGetRelid(rel), lockmode);
+
+       /*
+        * Find out the list of column names to process.  Fortunately, we
+        * already have the list of column numbers.
+        */
+       foreach(lc, unconstrained_cols)
+       {
+           colnames = lappend(colnames, get_attname(RelationGetRelid(rel),
+                                                    lfirst_int(lc), false));
+       }
+
+       foreach(child, children)
+       {
+           Oid         childrelid = lfirst_oid(child);
+           Relation    childrel;
+
+           if (list_member_oid(pkready, childrelid))
+               continue;       /* child already processed */
+
+           /* find_inheritance_children already got lock */
+           childrel = table_open(childrelid, NoLock);
+           CheckTableNotInUse(childrel, "ALTER TABLE");
+
+           foreach(lc, colnames)
+           {
+               HeapTuple   contup;
+               char       *colName = lfirst(lc);
+
+               contup = findNotNullConstraint(childrelid, colName);
+               if (contup == NULL)
+                   elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\", relation \"%s\"",
+                        colName, RelationGetRelationName(childrel));
+
+               dropconstraint_internal(childrel, contup,
+                                       DROP_RESTRICT, true, true,
+                                       false, &pkready,
+                                       lockmode);
+               pkready = NIL;
+           }
+
+           table_close(childrel, NoLock);
+
+           pkready = lappend_oid(pkready, childrelid);
+       }
+   }
+
    table_close(conrel, RowExclusiveLock);
+
+   return conobj;
 }
 
 /*
 
        /*
         * If the constraint is inherited (only), we don't want to inject a
-        * new definition here; it'll get recreated when ATAddCheckConstraint
-        * recurses from adding the parent table's constraint.  But we had to
-        * carry the info this far so that we can drop the constraint below.
+        * new definition here; it'll get recreated when
+        * ATAddCheckNNConstraint recurses from adding the parent table's
+        * constraint.  But we had to carry the info this far so that we can
+        * drop the constraint below.
         */
        if (!conislocal)
            continue;
                                             NIL,
                                             con->conname);
                }
-               else if (cmd->subtype == AT_SetNotNull)
+               else if (cmd->subtype == AT_SetAttNotNull)
                {
                    /*
-                    * The parser will create AT_SetNotNull subcommands for
+                    * The parser will create AT_AttSetNotNull subcommands for
                     * columns of PRIMARY KEY indexes/constraints, but we need
                     * not do anything with them here, because the columns'
                     * NOT NULL marks will already have been propagated into
    /* OK to create inheritance */
    CreateInheritance(child_rel, parent_rel);
 
+   /*
+    * If parent_rel has a primary key, then child_rel has not-null
+    * constraints that make these columns as non nullable.  Make those
+    * constraints as inherited.
+    */
+   ATInheritAdjustNotNulls(parent_rel, child_rel, 1);
+
    ObjectAddressSet(address, RelationRelationId,
                     RelationGetRelid(parent_rel));
 
 
            /*
             * Check child doesn't discard NOT NULL property.  (Other
-            * constraints are checked elsewhere.)
+            * constraints are checked elsewhere.)  However, if the constraint
+            * is NO INHERIT in the parent, this is allowed.
             */
            if (attribute->attnotnull && !childatt->attnotnull)
-               ereport(ERROR,
-                       (errcode(ERRCODE_DATATYPE_MISMATCH),
-                        errmsg("column \"%s\" in child table must be marked NOT NULL",
-                               attributeName)));
+           {
+               HeapTuple   contup;
+
+               contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
+                                                    attribute->attnum);
+               if (!((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
+                   ereport(ERROR,
+                           (errcode(ERRCODE_DATATYPE_MISMATCH),
+                            errmsg("column \"%s\" in child table must be marked NOT NULL",
+                                   attributeName)));
+           }
 
            /*
             * Child column must be generated if and only if parent column is.
    SysScanDesc parent_scan;
    ScanKeyData parent_key;
    HeapTuple   parent_tuple;
+   Oid         parent_relid = RelationGetRelid(parent_rel);
    bool        child_is_partition = false;
 
    catalog_relation = table_open(ConstraintRelationId, RowExclusiveLock);
    ScanKeyInit(&parent_key,
                Anum_pg_constraint_conrelid,
                BTEqualStrategyNumber, F_OIDEQ,
-               ObjectIdGetDatum(RelationGetRelid(parent_rel)));
+               ObjectIdGetDatum(parent_relid));
    parent_scan = systable_beginscan(catalog_relation, ConstraintRelidTypidNameIndexId,
                                     true, NULL, 1, &parent_key);
 
        HeapTuple   child_tuple;
        bool        found = false;
 
-       if (parent_con->contype != CONSTRAINT_CHECK)
+       if (parent_con->contype != CONSTRAINT_CHECK &&
+           parent_con->contype != CONSTRAINT_NOTNULL)
            continue;
 
        /* if the parent's constraint is marked NO INHERIT, it's not inherited */
            Form_pg_constraint child_con = (Form_pg_constraint) GETSTRUCT(child_tuple);
            HeapTuple   child_copy;
 
-           if (child_con->contype != CONSTRAINT_CHECK)
+           if (child_con->contype != parent_con->contype)
                continue;
 
-           if (strcmp(NameStr(parent_con->conname),
+           /*
+            * CHECK constraint are matched by name, NOT NULL ones by
+            * attribute number
+            */
+           if (child_con->contype == CONSTRAINT_CHECK &&
+               strcmp(NameStr(parent_con->conname),
                       NameStr(child_con->conname)) != 0)
                continue;
+           else if (child_con->contype == CONSTRAINT_NOTNULL)
+           {
+               AttrNumber  parent_attno = extractNotNullColumn(parent_tuple);
+               AttrNumber  child_attno = extractNotNullColumn(child_tuple);
+
+               if (strcmp(get_attname(parent_relid, parent_attno, false),
+                          get_attname(RelationGetRelid(child_rel), child_attno,
+                                      false)) != 0)
+                   continue;
+           }
 
-           if (!constraints_equivalent(parent_tuple, child_tuple, tuple_desc))
+           if (child_con->contype == CONSTRAINT_CHECK &&
+               !constraints_equivalent(parent_tuple, child_tuple, tuple_desc))
                ereport(ERROR,
                        (errcode(ERRCODE_DATATYPE_MISMATCH),
                         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 CHECK child constraint is "no inherit" then cannot
+            * merge.
+            *
+            * This is not desirable for not-null constraints, mostly because
+            * it breaks our pg_upgrade strategy, but it also makes sense on
+            * its own: if a child has its own not-null constraint and then
+            * acquires a parent with the same constraint, then we start to
+            * enforce that constraint for all the descendants of that child
+            * too, if any.
+            */
+           if (child_con->contype == CONSTRAINT_CHECK &&
+               child_con->connoinherit)
                ereport(ERROR,
                        (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
                         errmsg("constraint \"%s\" conflicts with non-inherited constraint on child table \"%s\"",
                ereport(ERROR,
                        errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
                        errmsg("too many inheritance parents"));
+           if (child_con->contype == CONSTRAINT_NOTNULL &&
+               child_con->connoinherit)
+               child_con->connoinherit = false;
 
            /*
             * In case of partitions, an inherited constraint must be
    /* Off to RemoveInheritance() where most of the work happens */
    RemoveInheritance(rel, parent_rel, false);
 
+   /*
+    * If parent_rel has a primary key, then child_rel has not-null
+    * constraints that make these columns as non nullable.  Mark those
+    * constraints as no longer inherited by this parent.
+    */
+   ATInheritAdjustNotNulls(parent_rel, rel, -1);
+
+   /*
+    * If the parent has a primary key, then we decrement counts for all NOT
+    * NULL constraints
+    */
+
    ObjectAddressSet(address, RelationRelationId,
                     RelationGetRelid(parent_rel));
 
    HeapTuple   attributeTuple,
                constraintTuple;
    List       *connames;
+   List       *nncolumns;
    bool        found;
    bool        child_is_partition = false;
 
     * this, we first need a list of the names of the parent's check
     * constraints.  (We cheat a bit by only checking for name matches,
     * assuming that the expressions will match.)
+    *
+    * For NOT NULL columns, we store column numbers to match.
     */
    catalogRelation = table_open(ConstraintRelationId, RowExclusiveLock);
    ScanKeyInit(&key[0],
                              true, NULL, 1, key);
 
    connames = NIL;
+   nncolumns = NIL;
 
    while (HeapTupleIsValid(constraintTuple = systable_getnext(scan)))
    {
 
        if (con->contype == CONSTRAINT_CHECK)
            connames = lappend(connames, pstrdup(NameStr(con->conname)));
+       if (con->contype == CONSTRAINT_NOTNULL)
+           nncolumns = lappend_int(nncolumns, extractNotNullColumn(constraintTuple));
    }
 
    systable_endscan(scan);
    while (HeapTupleIsValid(constraintTuple = systable_getnext(scan)))
    {
        Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple);
-       bool        match;
+       bool        match = false;
        ListCell   *lc;
 
-       if (con->contype != CONSTRAINT_CHECK)
-           continue;
-
-       match = false;
-       foreach(lc, connames)
+       /*
+        * Match CHECK constraints by name, not-null constraints by column
+        * number, and ignore all others.
+        */
+       if (con->contype == CONSTRAINT_CHECK)
        {
-           if (strcmp(NameStr(con->conname), (char *) lfirst(lc)) == 0)
+           foreach(lc, connames)
            {
-               match = true;
-               break;
+               if (con->contype == CONSTRAINT_CHECK &&
+                   strcmp(NameStr(con->conname), (char *) lfirst(lc)) == 0)
+               {
+                   match = true;
+                   break;
+               }
+           }
+       }
+       else if (con->contype == CONSTRAINT_NOTNULL)
+       {
+           AttrNumber  child_attno = extractNotNullColumn(constraintTuple);
+
+           foreach(lc, nncolumns)
+           {
+               if (lfirst_int(lc) == child_attno)
+               {
+                   match = true;
+                   break;
+               }
            }
        }
+       else
+           continue;
 
        if (match)
        {
                                 RelationGetRelid(parent_rel), false);
 }
 
+/*
+ * Adjust coninhcount of not-null constraints upwards or downwards when a
+ * table is marked as inheriting or no longer doing so a table with a primary
+ * key.
+ *
+ * Note: these constraints are not dropped, even if their inhcount goes to zero
+ * and conislocal is false.  Instead we mark the constraints as locally defined.
+ * This is seen as more useful behavior, with no downsides.  The user can always
+ * drop them afterwards.
+ */
+static void
+ATInheritAdjustNotNulls(Relation parent_rel, Relation child_rel, int inhcount)
+{
+   Bitmapset  *pkattnos;
+
+   /* Quick exit when parent has no PK */
+   if (!parent_rel->rd_rel->relhasindex)
+       return;
+
+   pkattnos = RelationGetIndexAttrBitmap(parent_rel,
+                                         INDEX_ATTR_BITMAP_PRIMARY_KEY);
+   if (pkattnos != NULL)
+   {
+       Bitmapset  *childattnums = NULL;
+       AttrMap    *attmap;
+       int         i;
+
+       attmap = build_attrmap_by_name(RelationGetDescr(parent_rel),
+                                      RelationGetDescr(child_rel), true);
+
+       i = -1;
+       while ((i = bms_next_member(pkattnos, i)) >= 0)
+       {
+           childattnums = bms_add_member(childattnums,
+                                         attmap->attnums[i + FirstLowInvalidHeapAttributeNumber - 1]);
+       }
+
+       /*
+        * CCI is needed in case there's a NOT NULL PRIMARY KEY column in the
+        * parent: the relevant not-null constraint in the child already had
+        * its inhcount modified earlier.
+        */
+       CommandCounterIncrement();
+       AdjustNotNullInheritance(RelationGetRelid(child_rel), childattnums,
+                                inhcount);
+   }
+}
+
 /*
  * Drop the dependency created by StoreCatalogInheritance1 (CREATE TABLE
  * INHERITS/ALTER TABLE INHERIT -- refclassid will be RelationRelationId) or
  *     Do scanrel's existing constraints imply the partition constraint?
  *
  * "Existing constraints" include its check constraints and column-level
- * NOT NULL constraints.  partConstraint describes the partition constraint,
+ * not-null constraints.  partConstraint describes the partition constraint,
  * in implicit-AND form.
  */
 bool
    StorePartitionBound(attachrel, rel, cmd->bound);
 
    /* Ensure there exists a correct set of indexes in the partition. */
-   AttachPartitionEnsureIndexes(rel, attachrel);
+   AttachPartitionEnsureIndexes(wqueue, rel, attachrel);
 
    /* and triggers */
    CloneRowTriggersToPartition(rel, attachrel);
  * partitioned table.
  */
 static void
-AttachPartitionEnsureIndexes(Relation rel, Relation attachrel)
+AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel)
 {
    List       *idxes;
    List       *attachRelIdxs;
    Relation   *attachrelIdxRels;
    IndexInfo **attachInfos;
-   int         i;
    ListCell   *cell;
    MemoryContext cxt;
    MemoryContext oldcxt;
    attachInfos = palloc(sizeof(IndexInfo *) * list_length(attachRelIdxs));
 
    /* Build arrays of all existing indexes and their IndexInfos */
-   i = 0;
    foreach(cell, attachRelIdxs)
    {
        Oid         cldIdxId = lfirst_oid(cell);
+       int         i = foreach_current_index(cell);
 
        attachrelIdxRels[i] = index_open(cldIdxId, AccessShareLock);
        attachInfos[i] = BuildIndexInfo(attachrelIdxRels[i]);
-       i++;
    }
 
    /*
         * the first matching, valid, unattached one we find, if any, as
         * partition of the parent index.  If we find one, we're done.
         */
-       for (i = 0; i < list_length(attachRelIdxs); i++)
+       for (int i = 0; i < list_length(attachRelIdxs); i++)
        {
            Oid         cldIdxId = RelationGetRelid(attachrelIdxRels[i]);
            Oid         cldConstrOid = InvalidOid;
            stmt = generateClonedIndexStmt(NULL,
                                           idxRel, attmap,
                                           &conOid);
+
+           /*
+            * If the index is a primary key, mark all columns as NOT NULL if
+            * they aren't already.
+            */
+           if (stmt->primary)
+           {
+               MemoryContextSwitchTo(oldcxt);
+               for (int j = 0; j < info->ii_NumIndexKeyAttrs; j++)
+               {
+                   AttrNumber  childattno;
+
+                   childattno = get_attnum(RelationGetRelid(attachrel),
+                                           get_attname(RelationGetRelid(rel),
+                                                       info->ii_IndexAttrNumbers[j],
+                                                       false));
+                   set_attnotnull(wqueue, attachrel, childattno,
+                                  true, AccessExclusiveLock);
+               }
+               MemoryContextSwitchTo(cxt);
+           }
+
            DefineIndex(RelationGetRelid(attachrel), stmt, InvalidOid,
                        RelationGetRelid(idxRel),
                        conOid,
 
 out:
    /* Clean up. */
-   for (i = 0; i < list_length(attachRelIdxs); i++)
+   for (int i = 0; i < list_length(attachRelIdxs); i++)
        index_close(attachrelIdxRels[i], AccessShareLock);
    MemoryContextSwitchTo(oldcxt);
    MemoryContextDelete(cxt);
        n->initially_valid = true;
        n->skip_validation = true;
        /* It's a re-add, since it nominally already exists */
-       ATAddCheckConstraint(wqueue, tab, partRel, n,
-                            true, false, true, ShareUpdateExclusiveLock);
+       ATAddCheckNNConstraint(wqueue, tab, partRel, n,
+                              true, false, true, ShareUpdateExclusiveLock);
    }
 }
 
                                   RelationGetRelationName(partIdx))));
        }
 
+       /*
+        * If it's a primary key, make sure the columns in the partition are
+        * NOT NULL.
+        */
+       if (parentIdx->rd_index->indisprimary)
+           verifyPartitionIndexNotNull(childInfo, partTbl);
+
        /* All good -- do it */
        IndexSetParentIndex(partIdx, RelationGetRelid(parentIdx));
        if (OidIsValid(constraintOid))
    }
 }
 
+/*
+ * When attaching an index as a partition of a partitioned index which is a
+ * primary key, verify that all the columns in the partition are marked NOT
+ * NULL.
+ */
+static void
+verifyPartitionIndexNotNull(IndexInfo *iinfo, Relation partition)
+{
+   for (int i = 0; i < iinfo->ii_NumIndexKeyAttrs; i++)
+   {
+       Form_pg_attribute att = TupleDescAttr(RelationGetDescr(partition),
+                                             iinfo->ii_IndexAttrNumbers[i] - 1);
+
+       if (!att->attnotnull)
+           ereport(ERROR,
+                   errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                   errmsg("invalid primary key definition"),
+                   errdetail("Column \"%s\" of relation \"%s\" is not marked NOT NULL.",
+                             NameStr(att->attname),
+                             RelationGetRelationName(partition)));
+   }
+}
+
 /*
  * Return an OID list of constraints that reference the given relation
  * that are marked as having a parent constraints.
 
 
        case CONSTR_NOTNULL:
            appendStringInfoString(str, "NOT_NULL");
+           WRITE_NODE_FIELD(keys);
+           WRITE_INT_FIELD(inhcount);
+           WRITE_BOOL_FIELD(is_no_inherit);
+           WRITE_BOOL_FIELD(skip_validation);
+           WRITE_BOOL_FIELD(initially_valid);
            break;
 
        case CONSTR_DEFAULT:
 
    switch (local_node->contype)
    {
        case CONSTR_NULL:
-       case CONSTR_NOTNULL:
            /* no extra fields */
            break;
 
+       case CONSTR_NOTNULL:
+           READ_NODE_FIELD(keys);
+           READ_INT_FIELD(inhcount);
+           READ_BOOL_FIELD(is_no_inherit);
+           READ_BOOL_FIELD(skip_validation);
+           READ_BOOL_FIELD(initially_valid);
+           break;
+
        case CONSTR_DEFAULT:
            READ_NODE_FIELD(raw_expr);
            READ_STRING_FIELD(cooked_expr);
 
     * Currently, attnotnull constraints must be treated as NO INHERIT unless
     * this is a partitioned table.  In future we might track their
     * inheritance status more accurately, allowing this to be refined.
+    *
+    * XXX do we need/want to change this?
     */
    include_notnull = (!rte->inh || rte->relkind == RELKIND_PARTITIONED_TABLE);
 
 
  * or be part of a_expr NOT LIKE or similar constructs).
  */
 ColConstraintElem:
-           NOT NULL_P
+           NOT NULL_P opt_no_inherit
                {
                    Constraint *n = makeNode(Constraint);
 
                    n->contype = CONSTR_NOTNULL;
                    n->location = @1;
+                   n->is_no_inherit = $3;
+                   n->skip_validation = false;
+                   n->initially_valid = true;
                    $$ = (Node *) n;
                }
            | NULL_P
                    n->initially_valid = !n->skip_validation;
                    $$ = (Node *) n;
                }
+           | NOT NULL_P ColId ConstraintAttributeSpec
+               {
+                   Constraint *n = makeNode(Constraint);
+
+                   n->contype = CONSTR_NOTNULL;
+                   n->location = @1;
+                   n->keys = list_make1(makeString($3));
+                   /* no NOT VALID support yet */
+                   processCASbits($4, @4, "NOT NULL",
+                                  NULL, NULL, NULL,
+                                  &n->is_no_inherit, yyscanner);
+                   n->initially_valid = true;
+                   $$ = (Node *) n;
+               }
            | UNIQUE opt_unique_null_treatment '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
                ConstraintAttributeSpec
                {
 
    bool        isalter;        /* true if altering existing table */
    List       *columns;        /* ColumnDef items */
    List       *ckconstraints;  /* CHECK constraints */
+   List       *nnconstraints;  /* NOT NULL constraints */
    List       *fkconstraints;  /* FOREIGN KEY constraints */
    List       *ixconstraints;  /* index-creating constraints */
    List       *likeclauses;    /* LIKE clauses that need post-processing */
    cxt.isalter = false;
    cxt.columns = NIL;
    cxt.ckconstraints = NIL;
+   cxt.nnconstraints = NIL;
    cxt.fkconstraints = NIL;
    cxt.ixconstraints = NIL;
    cxt.likeclauses = NIL;
     */
    stmt->tableElts = cxt.columns;
    stmt->constraints = cxt.ckconstraints;
+   stmt->nnconstraints = cxt.nnconstraints;
 
    result = lappend(cxt.blist, stmt);
    result = list_concat(result, cxt.alist);
    bool        saw_default;
    bool        saw_identity;
    bool        saw_generated;
+   bool        need_notnull = false;
    ListCell   *clist;
 
    cxt->columns = lappend(cxt->columns, column);
        constraint->cooked_expr = NULL;
        column->constraints = lappend(column->constraints, constraint);
 
-       constraint = makeNode(Constraint);
-       constraint->contype = CONSTR_NOTNULL;
-       constraint->location = -1;
-       column->constraints = lappend(column->constraints, constraint);
+       /* have a not-null constraint added later */
+       need_notnull = true;
    }
 
    /* Process column constraints, if any... */
        switch (constraint->contype)
        {
            case CONSTR_NULL:
-               if (saw_nullable && column->is_not_null)
+               if ((saw_nullable && column->is_not_null) || need_notnull)
                    ereport(ERROR,
                            (errcode(ERRCODE_SYNTAX_ERROR),
                             errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"",
                break;
 
            case CONSTR_NOTNULL:
+
+               /*
+                * Disallow conflicting [NOT] NULL markings
+                */
                if (saw_nullable && !column->is_not_null)
                    ereport(ERROR,
                            (errcode(ERRCODE_SYNTAX_ERROR),
                                    column->colname, cxt->relation->relname),
                             parser_errposition(cxt->pstate,
                                                constraint->location)));
-               column->is_not_null = true;
-               saw_nullable = true;
+               /* Ignore redundant NOT NULL markings */
+
+               /*
+                * If this is the first time we see this column being marked
+                * not null, add the constraint entry; and get rid of any
+                * previous markings to mark the column NOT NULL.
+                */
+               if (!column->is_not_null)
+               {
+                   column->is_not_null = true;
+                   saw_nullable = true;
+
+                   constraint->keys = list_make1(makeString(column->colname));
+                   cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
+
+                   /* Don't need this anymore, if we had it */
+                   need_notnull = false;
+               }
+
                break;
 
            case CONSTR_DEFAULT:
                    column->identity = constraint->generated_when;
                    saw_identity = true;
 
-                   /* An identity column is implicitly NOT NULL */
-                   if (saw_nullable && !column->is_not_null)
+                   /*
+                    * Identity columns are always NOT NULL, but we may have a
+                    * constraint already.
+                    */
+                   if (!saw_nullable)
+                       need_notnull = true;
+                   else if (!column->is_not_null)
                        ereport(ERROR,
                                (errcode(ERRCODE_SYNTAX_ERROR),
                                 errmsg("conflicting NULL/NOT NULL declarations for column \"%s\" of table \"%s\"",
                                        column->colname, cxt->relation->relname),
                                 parser_errposition(cxt->pstate,
                                                    constraint->location)));
-                   column->is_not_null = true;
-                   saw_nullable = true;
                    break;
                }
 
                                        constraint->location)));
    }
 
+   /*
+    * If we need a not-null constraint for SERIAL or IDENTITY, and one was
+    * not explicitly specified, add one now.
+    */
+   if (need_notnull && !(saw_nullable && column->is_not_null))
+   {
+       Constraint *notnull;
+
+       column->is_not_null = true;
+
+       notnull = makeNode(Constraint);
+       notnull->contype = CONSTR_NOTNULL;
+       notnull->conname = NULL;
+       notnull->deferrable = false;
+       notnull->initdeferred = false;
+       notnull->location = -1;
+       notnull->keys = list_make1(makeString(column->colname));
+       notnull->skip_validation = false;
+       notnull->initially_valid = true;
+
+       cxt->nnconstraints = lappend(cxt->nnconstraints, notnull);
+   }
+
    /*
     * If needed, generate ALTER FOREIGN TABLE ALTER COLUMN statement to add
     * per-column foreign data wrapper options to this column after creation.
            cxt->ckconstraints = lappend(cxt->ckconstraints, constraint);
            break;
 
+       case CONSTR_NOTNULL:
+           cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
+           break;
+
        case CONSTR_FOREIGN:
            if (cxt->isforeign)
                ereport(ERROR,
            break;
 
        case CONSTR_NULL:
-       case CONSTR_NOTNULL:
        case CONSTR_DEFAULT:
        case CONSTR_ATTR_DEFERRABLE:
        case CONSTR_ATTR_NOT_DEFERRABLE:
    AclResult   aclresult;
    char       *comment;
    ParseCallbackState pcbstate;
+   bool        process_notnull_constraints = false;
 
    setup_parser_errposition_callback(&pcbstate, cxt->pstate,
                                      table_like_clause->relation->location);
        /*
         * Create a new column, which is marked as NOT inherited.
         *
-        * For constraints, ONLY the NOT NULL constraint is inherited by the
-        * new column definition per SQL99.
+        * For constraints, ONLY the not-null constraint is inherited by the
+        * new column definition per SQL99; however we cannot do that
+        * correctly here, so we leave it for expandTableLikeClause to handle.
         */
        def = makeNode(ColumnDef);
        def->colname = pstrdup(attributeName);
                                            attribute->atttypmod);
        def->inhcount = 0;
        def->is_local = true;
-       def->is_not_null = attribute->attnotnull;
+       def->is_not_null = false;
+       if (attribute->attnotnull)
+           process_notnull_constraints = true;
        def->is_from_type = false;
        def->storage = 0;
        def->raw_default = NULL;
     * we don't yet know what column numbers the copied columns will have in
     * the finished table.  If any of those options are specified, add the
     * LIKE clause to cxt->likeclauses so that expandTableLikeClause will be
-    * called after we do know that.  Also, remember the relation OID so that
+    * called after we do know that; in addition, do that if there are any NOT
+    * NULL constraints, because those must be propagated even if not
+    * explicitly requested.
+    *
+    * In order for this to work, we remember the relation OID so that
     * expandTableLikeClause is certain to open the same table.
     */
-   if (table_like_clause->options &
-       (CREATE_TABLE_LIKE_DEFAULTS |
-        CREATE_TABLE_LIKE_GENERATED |
-        CREATE_TABLE_LIKE_CONSTRAINTS |
-        CREATE_TABLE_LIKE_INDEXES))
+   if ((table_like_clause->options &
+        (CREATE_TABLE_LIKE_DEFAULTS |
+         CREATE_TABLE_LIKE_GENERATED |
+         CREATE_TABLE_LIKE_CONSTRAINTS |
+         CREATE_TABLE_LIKE_INDEXES)) ||
+       process_notnull_constraints)
    {
        table_like_clause->relationOid = RelationGetRelid(relation);
        cxt->likeclauses = lappend(cxt->likeclauses, table_like_clause);
    }
 
+   /*
+    * If INCLUDING INDEXES is not given and a primary key exists, we need to
+    * add not-null constraints to the columns covered by the PK (except those
+    * that already have one.)  This is required for backwards compatibility.
+    */
+   if ((table_like_clause->options & CREATE_TABLE_LIKE_INDEXES) == 0)
+   {
+       Bitmapset  *pkcols;
+       int         x = -1;
+       Bitmapset  *donecols = NULL;
+       ListCell   *lc;
+
+       /*
+        * Obtain a bitmapset of columns on which we'll add not-null
+        * constraints in expandTableLikeClause, so that we skip this for
+        * those.
+        */
+       foreach(lc, RelationGetNotNullConstraints(RelationGetRelid(relation), true))
+       {
+           CookedConstraint *cooked = (CookedConstraint *) lfirst(lc);
+
+           donecols = bms_add_member(donecols, cooked->attnum);
+       }
+
+       pkcols = RelationGetIndexAttrBitmap(relation,
+                                           INDEX_ATTR_BITMAP_PRIMARY_KEY);
+       while ((x = bms_next_member(pkcols, x)) >= 0)
+       {
+           Constraint *notnull;
+           AttrNumber  attnum = x + FirstLowInvalidHeapAttributeNumber;
+           Form_pg_attribute attForm;
+
+           /* ignore if we already have one for this column */
+           if (bms_is_member(attnum, donecols))
+               continue;
+
+           attForm = TupleDescAttr(tupleDesc, attnum - 1);
+
+           notnull = makeNode(Constraint);
+           notnull->contype = CONSTR_NOTNULL;
+           notnull->conname = NULL;
+           notnull->is_no_inherit = false;
+           notnull->deferrable = false;
+           notnull->initdeferred = false;
+           notnull->location = -1;
+           notnull->keys = list_make1(makeString(pstrdup(NameStr(attForm->attname))));
+           notnull->skip_validation = false;
+           notnull->initially_valid = true;
+
+           cxt->nnconstraints = lappend(cxt->nnconstraints, notnull);
+       }
+   }
+
    /*
     * We may copy extended statistics if requested, since the representation
     * of CreateStatsStmt doesn't depend on column numbers.
    TupleConstr *constr;
    AttrMap    *attmap;
    char       *comment;
+   bool        at_pushed = false;
+   ListCell   *lc;
 
    /*
     * Open the relation referenced by the LIKE clause.  We should still have
        }
    }
 
+   /*
+    * Copy not-null constraints, too (these do not require any option to have
+    * been given).
+    */
+   foreach(lc, RelationGetNotNullConstraints(RelationGetRelid(relation), false))
+   {
+       AlterTableCmd *atsubcmd;
+
+       atsubcmd = makeNode(AlterTableCmd);
+       atsubcmd->subtype = AT_AddConstraint;
+       atsubcmd->def = (Node *) lfirst_node(Constraint, lc);
+       atsubcmds = lappend(atsubcmds, atsubcmd);
+   }
+
    /*
     * If we generated any ALTER TABLE actions above, wrap them into a single
     * ALTER TABLE command.  Stick it at the front of the result, so it runs
        atcmd->objtype = OBJECT_TABLE;
        atcmd->missing_ok = false;
        result = lcons(atcmd, result);
+
+       at_pushed = true;
    }
 
    /*
                                                 attmap,
                                                 NULL);
 
+           /*
+            * The PK columns might not yet non-nullable, so make sure they
+            * become so.
+            */
+           if (index_stmt->primary)
+           {
+               foreach(lc, index_stmt->indexParams)
+               {
+                   IndexElem  *col = lfirst_node(IndexElem, lc);
+                   AlterTableCmd *notnullcmd = makeNode(AlterTableCmd);
+
+                   notnullcmd->subtype = AT_SetAttNotNull;
+                   notnullcmd->name = pstrdup(col->name);
+                   /* Luckily we can still add more AT-subcmds here */
+                   atsubcmds = lappend(atsubcmds, notnullcmd);
+               }
+
+               /*
+                * If we had already put the AlterTableStmt into the output
+                * list, we don't need to do so again; otherwise do it.
+                */
+               if (!at_pushed)
+               {
+                   AlterTableStmt *atcmd = makeNode(AlterTableStmt);
+
+                   atcmd->relation = copyObject(heapRel);
+                   atcmd->cmds = atsubcmds;
+                   atcmd->objtype = OBJECT_TABLE;
+                   atcmd->missing_ok = false;
+                   result = lcons(atcmd, result);
+               }
+           }
+
            /* Copy comment on index, if requested */
            if (table_like_clause->options & CREATE_TABLE_LIKE_COMMENTS)
            {
  * with the index there.
  *
  * Unlike transformIndexConstraint, we don't make any effort to force primary
- * key columns to be NOT NULL.  The larger cloning process this is part of
- * should have cloned their NOT NULL status separately (and DefineIndex will
+ * key columns to be not-null.  The larger cloning process this is part of
+ * should have cloned their not-null status separately (and DefineIndex will
  * complain if that fails to happen).
  */
 IndexStmt *
    ListCell   *lc;
 
    /*
-    * Run through the constraints that need to generate an index. For PRIMARY
-    * KEY, mark each column as NOT NULL and create an index. For UNIQUE or
-    * EXCLUDE, create an index as for PRIMARY KEY, but do not insist on NOT
-    * NULL.
+    * Run through the constraints that need to generate an index, and do so.
+    *
+    * For PRIMARY KEY, in addition we set each column's attnotnull flag true.
+    * We do not create a separate not-null constraint, as that would be
+    * redundant: the PRIMARY KEY constraint itself fulfills that role.  Other
+    * constraint types don't need any not-null markings.
     */
    foreach(lc, cxt->ixconstraints)
    {
    }
 
    /*
-    * Now append all the IndexStmts to cxt->alist.  If we generated an ALTER
-    * TABLE SET NOT NULL statement to support a primary key, it's already in
-    * cxt->alist.
+    * Now append all the IndexStmts to cxt->alist.
     */
    cxt->alist = list_concat(cxt->alist, finalindexlist);
 }
 /*
  * transformIndexConstraint
  *     Transform one UNIQUE, PRIMARY KEY, or EXCLUDE constraint for
- *     transformIndexConstraints.
+ *     transformIndexConstraints. An IndexStmt is returned.
  *
- * We return an IndexStmt.  For a PRIMARY KEY constraint, we additionally
- * produce NOT NULL constraints, either by marking ColumnDefs in cxt->columns
- * as is_not_null or by adding an ALTER TABLE SET NOT NULL command to
- * cxt->alist.
+ * For a PRIMARY KEY constraint, we additionally force the columns to be
+ * marked as not-null, without producing a not-null constraint.
  */
 static IndexStmt *
 transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
     * For UNIQUE and PRIMARY KEY, we just have a list of column names.
     *
     * Make sure referenced keys exist.  If we are making a PRIMARY KEY index,
-    * also make sure they are NOT NULL.
+    * also make sure they are not-null.
     */
    else
    {
        {
            char       *key = strVal(lfirst(lc));
            bool        found = false;
-           bool        forced_not_null = false;
            ColumnDef  *column = NULL;
            ListCell   *columns;
            IndexElem  *iparam;
            {
                /*
                 * column is defined in the new table.  For PRIMARY KEY, we
-                * can apply the NOT NULL constraint cheaply here ... unless
+                * can apply the not-null constraint cheaply here ... unless
                 * the column is marked is_from_type, in which case marking it
-                * here would be ineffective (see MergeAttributes).
+                * here would be ineffective (see MergeAttributes).  Note that
+                * this isn't effective in ALTER TABLE either, unless the
+                * column is being added in the same command.
                 */
                if (constraint->contype == CONSTR_PRIMARY &&
                    !column->is_from_type)
                {
                    column->is_not_null = true;
-                   forced_not_null = true;
                }
            }
            else if (SystemAttributeByName(key) != NULL)
                        if (strcmp(key, inhname) == 0)
                        {
                            found = true;
-
-                           /*
-                            * It's tempting to set forced_not_null if the
-                            * parent column is already NOT NULL, but that
-                            * seems unsafe because the column's NOT NULL
-                            * marking might disappear between now and
-                            * execution.  Do the runtime check to be safe.
-                            */
                            break;
                        }
                    }
            iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
            index->indexParams = lappend(index->indexParams, iparam);
 
-           /*
-            * For a primary-key column, also create an item for ALTER TABLE
-            * SET NOT NULL if we couldn't ensure it via is_not_null above.
-            */
-           if (constraint->contype == CONSTR_PRIMARY && !forced_not_null)
+           if (constraint->contype == CONSTR_PRIMARY)
            {
                AlterTableCmd *notnullcmd = makeNode(AlterTableCmd);
 
-               notnullcmd->subtype = AT_SetNotNull;
+               notnullcmd->subtype = AT_SetAttNotNull;
                notnullcmd->name = pstrdup(key);
                notnullcmds = lappend(notnullcmds, notnullcmd);
            }
    cxt.isalter = true;
    cxt.columns = NIL;
    cxt.ckconstraints = NIL;
+   cxt.nnconstraints = NIL;
    cxt.fkconstraints = NIL;
    cxt.ixconstraints = NIL;
    cxt.likeclauses = NIL;
 
        /*
         * We assume here that cxt.alist contains only IndexStmts and possibly
-        * ALTER TABLE SET NOT NULL statements generated from primary key
-        * constraints.  We absorb the subcommands of the latter directly.
+        * AT_SetAttNotNull statements generated from primary key constraints.
+        * We absorb the subcommands of the latter directly.
         */
        if (IsA(istmt, IndexStmt))
        {
    }
    cxt.alist = NIL;
 
-   /* Append any CHECK or FK constraints to the commands list */
+   /* Append any CHECK, NOT NULL or FK constraints to the commands list */
    foreach(l, cxt.ckconstraints)
    {
        newcmd = makeNode(AlterTableCmd);
        newcmd->subtype = AT_AddConstraint;
-       newcmd->def = (Node *) lfirst(l);
+       newcmd->def = (Node *) lfirst_node(Constraint, l);
+       newcmds = lappend(newcmds, newcmd);
+   }
+   foreach(l, cxt.nnconstraints)
+   {
+       newcmd = makeNode(AlterTableCmd);
+       newcmd->subtype = AT_AddConstraint;
+       newcmd->def = (Node *) lfirst_node(Constraint, l);
        newcmds = lappend(newcmds, newcmd);
    }
    foreach(l, cxt.fkconstraints)
    {
        newcmd = makeNode(AlterTableCmd);
        newcmd->subtype = AT_AddConstraint;
-       newcmd->def = (Node *) lfirst(l);
+       newcmd->def = (Node *) lfirst_node(Constraint, l);
        newcmds = lappend(newcmds, newcmd);
    }
 
 
                                 conForm->connoinherit ? " NO INHERIT" : "");
                break;
            }
+       case CONSTRAINT_NOTNULL:
+           {
+               AttrNumber  attnum;
+
+               attnum = extractNotNullColumn(tup);
+
+               appendStringInfo(&buf, "NOT NULL %s",
+                                quote_identifier(get_attname(conForm->conrelid,
+                                                             attnum, false)));
+               if (((Form_pg_constraint) GETSTRUCT(tup))->connoinherit)
+                   appendStringInfoString(&buf, " NO INHERIT");
+               break;
+           }
+
        case CONSTRAINT_TRIGGER:
 
            /*
 
        result = lappend_oid(result, index->indexrelid);
 
        /*
-        * Invalid, non-unique, non-immediate or predicate indexes aren't
-        * interesting for either oid indexes or replication identity indexes,
-        * so don't check them.
+        * Non-unique, non-immediate or predicate indexes aren't interesting
+        * for either oid indexes or replication identity indexes, so don't
+        * check them.
         */
-       if (!index->indisvalid || !index->indisunique ||
+       if (!index->indisunique ||
            !index->indimmediate ||
            !heap_attisnull(htup, Anum_pg_index_indpred, NULL))
            continue;
 
-       /* remember primary key index if any */
-       if (index->indisprimary)
+       /*
+        * Remember primary key index, if any.  We do this only if the index
+        * is valid; but if the table is partitioned, then we do it even if
+        * it's invalid.
+        *
+        * The reason for returning invalid primary keys for foreign tables is
+        * because of pg_dump of NOT NULL constraints, and the fact that PKs
+        * remain marked invalid until the partitions' PKs are attached to it.
+        * If we make rd_pkindex invalid, then the attnotnull flag is reset
+        * after the PK is created, which causes the ALTER INDEX ATTACH
+        * PARTITION to fail with 'column ... is not marked NOT NULL'.  With
+        * this, dropconstraint_internal() will believe that the columns must
+        * not have attnotnull reset, so the PKs-on-partitions can be attached
+        * correctly, until finally the PK-on-parent is marked valid.
+        *
+        * Also, this doesn't harm anything, because rd_pkindex is not a
+        * "real" index anyway, but a RELKIND_PARTITIONED_INDEX.
+        */
+       if (index->indisprimary &&
+           (index->indisvalid ||
+            relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
            pkeyIndex = index->indexrelid;
 
+       if (!index->indisvalid)
+           continue;
+
        /* remember explicitly chosen replica index */
        if (index->indisreplident)
            candidateIndex = index->indexrelid;
 
 static void flagInhTables(Archive *fout, TableInfo *tblinfo, int numTables,
                          InhInfo *inhinfo, int numInherits);
 static void flagInhIndexes(Archive *fout, TableInfo *tblinfo, int numTables);
-static void flagInhAttrs(DumpOptions *dopt, TableInfo *tblinfo, int numTables);
+static void flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo,
+                        int numTables);
 static int strInArray(const char *pattern, char **arr, int arr_size);
 static IndxInfo *findIndexByOid(Oid oid);
 
    getTableAttrs(fout, tblinfo, numTables);
 
    pg_log_info("flagging inherited columns in subtables");
-   flagInhAttrs(fout->dopt, tblinfo, numTables);
+   flagInhAttrs(fout, fout->dopt, tblinfo, numTables);
 
    pg_log_info("reading partitioning data");
    getPartitioningInfo(fout);
  * What we need to do here is:
  *
  * - Detect child columns that inherit NOT NULL bits from their parents, so
- *   that we needn't specify that again for the child.
+ *   that we needn't specify that again for the child. (Versions >= 16 no
+ *   longer need this.)
  *
  * - Detect child columns that have DEFAULT NULL when their parents had some
  *   non-null default.  In this case, we make up a dummy AttrDefInfo object so
  * modifies tblinfo
  */
 static void
-flagInhAttrs(DumpOptions *dopt, TableInfo *tblinfo, int numTables)
+flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, int numTables)
 {
    int         i,
                j,
                {
                    AttrDefInfo *parentDef = parent->attrdefs[inhAttrInd];
 
-                   foundNotNull |= parent->notnull[inhAttrInd];
+                   foundNotNull |= (parent->notnull_constrs[inhAttrInd] != NULL &&
+                                    !parent->notnull_noinh[inhAttrInd]);
                    foundDefault |= (parentDef != NULL &&
                                     strcmp(parentDef->adef_expr, "NULL") != 0 &&
                                     !parent->attgenerated[inhAttrInd]);
                }
            }
 
-           /* Remember if we found inherited NOT NULL */
-           tbinfo->inhNotNull[j] = foundNotNull;
+           /* In versions < 17, remember if we found inherited NOT NULL */
+           if (fout->remoteVersion < 170000)
+               tbinfo->notnull_inh[j] = foundNotNull;
 
            /*
             * Manufacture a DEFAULT NULL clause if necessary.  This breaks
 
        i_extname = PQfnumber(res, "extname");
        for (i = 0; i < ntups; i++)
        {
-           appendPQExpBuffer(create, "ALTER %s %s DEPENDS ON EXTENSION %s;\n",
+           appendPQExpBuffer(create, "\nALTER %s %s DEPENDS ON EXTENSION %s;",
                              keyword, nm,
                              fmtId(PQgetvalue(res, i, i_extname)));
        }
    int         i_attlen;
    int         i_attalign;
    int         i_attislocal;
-   int         i_attnotnull;
+   int         i_notnull_name;
+   int         i_notnull_noinherit;
+   int         i_notnull_is_pk;
+   int         i_notnull_inh;
    int         i_attoptions;
    int         i_attcollation;
    int         i_attcompression;
 
    /*
     * We want to perform just one query against pg_attribute, and then just
-    * one against pg_attrdef (for DEFAULTs) and one against pg_constraint
-    * (for CHECK constraints).  However, we mustn't try to select every row
-    * of those catalogs and then sort it out on the client side, because some
-    * of the server-side functions we need would be unsafe to apply to tables
-    * we don't have lock on.  Hence, we build an array of the OIDs of tables
-    * we care about (and now have lock on!), and use a WHERE clause to
-    * constrain which rows are selected.
+    * one against pg_attrdef (for DEFAULTs) and two against pg_constraint
+    * (for CHECK constraints and for NOT NULL constraints).  However, we
+    * mustn't try to select every row of those catalogs and then sort it out
+    * on the client side, because some of the server-side functions we need
+    * would be unsafe to apply to tables we don't have lock on.  Hence, we
+    * build an array of the OIDs of tables we care about (and now have lock
+    * on!), and use a WHERE clause to constrain which rows are selected.
     */
    appendPQExpBufferChar(tbloids, '{');
    appendPQExpBufferChar(checkoids, '{');
                         "a.attstattarget,\n"
                         "a.attstorage,\n"
                         "t.typstorage,\n"
-                        "a.attnotnull,\n"
                         "a.atthasdef,\n"
                         "a.attisdropped,\n"
                         "a.attlen,\n"
                         "ORDER BY option_name"
                         "), E',\n    ') AS attfdwoptions,\n");
 
+   /*
+    * Find out any NOT NULL markings for each column.  In 17 and up we have
+    * to read pg_constraint, and keep track whether it's NO INHERIT; in older
+    * versions we rely on pg_attribute.attnotnull.
+    *
+    * We also track whether the constraint was defined directly in this table
+    * or via an ancestor, for binary upgrade.
+    *
+    * Lastly, we need to know if the PK for the table involves each column;
+    * for columns that are there we need a NOT NULL marking even if there's
+    * no explicit constraint, to avoid the table having to be scanned for
+    * NULLs after the data is loaded when the PK is created, later in the
+    * dump; for this case we add throwaway constraints that are dropped once
+    * the PK is created.
+    */
+   if (fout->remoteVersion >= 170000)
+       appendPQExpBufferStr(q,
+                            "co.conname AS notnull_name,\n"
+                            "co.connoinherit AS notnull_noinherit,\n"
+                            "copk.conname IS NOT NULL as notnull_is_pk,\n"
+                            "coalesce(NOT co.conislocal, true) AS notnull_inh,\n");
+   else
+       appendPQExpBufferStr(q,
+                            "CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n"
+                            "false AS notnull_noinherit,\n"
+                            "copk.conname IS NOT NULL AS notnull_is_pk,\n"
+                            "NOT a.attislocal AS notnull_inh,\n");
+
    if (fout->remoteVersion >= 140000)
        appendPQExpBufferStr(q,
                             "a.attcompression AS attcompression,\n");
                      "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
                      "JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) "
                      "LEFT JOIN pg_catalog.pg_type t "
-                     "ON (a.atttypid = t.oid)\n"
-                     "WHERE a.attnum > 0::pg_catalog.int2\n"
-                     "ORDER BY a.attrelid, a.attnum",
+                     "ON (a.atttypid = t.oid)\n",
                      tbloids->data);
 
+   /*
+    * In versions 16 and up, we need pg_constraint for explicit NOT NULL
+    * entries.  Also, we need to know if the NOT NULL for each column is
+    * backing a primary key.
+    */
+   if (fout->remoteVersion >= 170000)
+       appendPQExpBufferStr(q,
+                            " LEFT JOIN pg_catalog.pg_constraint co ON "
+                            "(a.attrelid = co.conrelid\n"
+                            "   AND co.contype = 'n' AND "
+                            "co.conkey = array[a.attnum])\n");
+
+   appendPQExpBufferStr(q,
+                        "LEFT JOIN pg_catalog.pg_constraint copk ON "
+                        "(copk.conrelid = src.tbloid\n"
+                        "   AND copk.contype = 'p' AND "
+                        "copk.conkey @> array[a.attnum])\n"
+                        "WHERE a.attnum > 0::pg_catalog.int2\n"
+                        "ORDER BY a.attrelid, a.attnum");
+
    res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
 
    ntups = PQntuples(res);
    i_attlen = PQfnumber(res, "attlen");
    i_attalign = PQfnumber(res, "attalign");
    i_attislocal = PQfnumber(res, "attislocal");
-   i_attnotnull = PQfnumber(res, "attnotnull");
+   i_notnull_name = PQfnumber(res, "notnull_name");
+   i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
+   i_notnull_is_pk = PQfnumber(res, "notnull_is_pk");
+   i_notnull_inh = PQfnumber(res, "notnull_inh");
    i_attoptions = PQfnumber(res, "attoptions");
    i_attcollation = PQfnumber(res, "attcollation");
    i_attcompression = PQfnumber(res, "attcompression");
        TableInfo  *tbinfo = NULL;
        int         numatts;
        bool        hasdefaults;
+       int         notnullcount;
 
        /* Count rows for this table */
        for (numatts = 1; numatts < ntups - r; numatts++)
            pg_fatal("unexpected column data for table \"%s\"",
                     tbinfo->dobj.name);
 
+       notnullcount = 0;
+
        /* Save data for this table */
        tbinfo->numatts = numatts;
        tbinfo->attnames = (char **) pg_malloc(numatts * sizeof(char *));
        tbinfo->attcompression = (char *) pg_malloc(numatts * sizeof(char));
        tbinfo->attfdwoptions = (char **) pg_malloc(numatts * sizeof(char *));
        tbinfo->attmissingval = (char **) pg_malloc(numatts * sizeof(char *));
-       tbinfo->notnull = (bool *) pg_malloc(numatts * sizeof(bool));
-       tbinfo->inhNotNull = (bool *) pg_malloc(numatts * sizeof(bool));
+       tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *));
+       tbinfo->notnull_noinh = (bool *) pg_malloc(numatts * sizeof(bool));
+       tbinfo->notnull_throwaway = (bool *) pg_malloc(numatts * sizeof(bool));
+       tbinfo->notnull_inh = (bool *) pg_malloc(numatts * sizeof(bool));
        tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *));
        hasdefaults = false;
 
        for (int j = 0; j < numatts; j++, r++)
        {
+           bool        use_named_notnull = false;
+           bool        use_unnamed_notnull = false;
+           bool        use_throwaway_notnull = false;
+
            if (j + 1 != atoi(PQgetvalue(res, r, i_attnum)))
                pg_fatal("invalid column numbering in table \"%s\"",
                         tbinfo->dobj.name);
            tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
            tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
            tbinfo->attislocal[j] = (PQgetvalue(res, r, i_attislocal)[0] == 't');
-           tbinfo->notnull[j] = (PQgetvalue(res, r, i_attnotnull)[0] == 't');
+
+           /*
+            * Not-null constraints require a jumping through a few hoops.
+            * First, if the user has specified a constraint name that's not
+            * the system-assigned default name, then we need to preserve
+            * that. But if they haven't, then we don't want to use the
+            * verbose syntax in the dump output. (Also, in versions prior to
+            * 17, there was no constraint name at all.)
+            *
+            * (XXX Comparing the name this way to a supposed default name is
+            * a bit of a hack, but it beats having to store a boolean flag in
+            * pg_constraint just for this, or having to compute the knowledge
+            * at pg_dump time from the server.)
+            *
+            * We also need to know if a column is part of the primary key. In
+            * that case, we want to mark the column as not-null at table
+            * creation time, so that the table doesn't have to be scanned to
+            * check for nulls when the PK is created afterwards; this is
+            * especially critical during pg_upgrade (where the data would not
+            * be scanned at all otherwise.)  If the column is part of the PK
+            * and does not have any other not-null constraint, then we
+            * fabricate a throwaway constraint name that we later use to
+            * remove the constraint after the PK has been created.
+            *
+            * For inheritance child tables, we don't want to print not-null
+            * when the constraint was defined at the parent level instead of
+            * locally.
+            */
+
+           /*
+            * We use notnull_inh to suppress unwanted not-null constraints in
+            * inheritance children, when said constraints come from the
+            * parent(s).
+            */
+           tbinfo->notnull_inh[j] = PQgetvalue(res, r, i_notnull_inh)[0] == 't';
+
+           if (fout->remoteVersion < 170000)
+           {
+               if (!PQgetisnull(res, r, i_notnull_name) &&
+                   dopt->binary_upgrade &&
+                   !tbinfo->ispartition &&
+                   tbinfo->notnull_inh[j])
+               {
+                   use_named_notnull = true;
+                   /* XXX should match ChooseConstraintName better */
+                   tbinfo->notnull_constrs[j] =
+                       psprintf("%s_%s_not_null", tbinfo->dobj.name,
+                                tbinfo->attnames[j]);
+               }
+               else if (PQgetvalue(res, r, i_notnull_is_pk)[0] == 't')
+                   use_throwaway_notnull = true;
+               else if (!PQgetisnull(res, r, i_notnull_name))
+                   use_unnamed_notnull = true;
+           }
+           else
+           {
+               if (!PQgetisnull(res, r, i_notnull_name))
+               {
+                   /*
+                    * In binary upgrade of inheritance child tables, must
+                    * have a constraint name that we can UPDATE later.
+                    */
+                   if (dopt->binary_upgrade &&
+                       !tbinfo->ispartition &&
+                       tbinfo->notnull_inh[j])
+                   {
+                       use_named_notnull = true;
+                       tbinfo->notnull_constrs[j] =
+                           pstrdup(PQgetvalue(res, r, i_notnull_name));
+
+                   }
+                   else
+                   {
+                       char       *default_name;
+
+                       /* XXX should match ChooseConstraintName better */
+                       default_name = psprintf("%s_%s_not_null", tbinfo->dobj.name,
+                                               tbinfo->attnames[j]);
+                       if (strcmp(default_name,
+                                  PQgetvalue(res, r, i_notnull_name)) == 0)
+                           use_unnamed_notnull = true;
+                       else
+                       {
+                           use_named_notnull = true;
+                           tbinfo->notnull_constrs[j] =
+                               pstrdup(PQgetvalue(res, r, i_notnull_name));
+                       }
+                   }
+               }
+               else if (PQgetvalue(res, r, i_notnull_is_pk)[0] == 't')
+                   use_throwaway_notnull = true;
+           }
+
+           if (use_unnamed_notnull)
+           {
+               tbinfo->notnull_constrs[j] = "";
+               tbinfo->notnull_throwaway[j] = false;
+           }
+           else if (use_named_notnull)
+           {
+               /* The name itself has already been determined */
+               tbinfo->notnull_throwaway[j] = false;
+           }
+           else if (use_throwaway_notnull)
+           {
+               tbinfo->notnull_constrs[j] =
+                   psprintf("pgdump_throwaway_notnull_%d", notnullcount++);
+               tbinfo->notnull_throwaway[j] = true;
+               tbinfo->notnull_inh[j] = false;
+           }
+           else
+           {
+               tbinfo->notnull_constrs[j] = NULL;
+               tbinfo->notnull_throwaway[j] = false;
+           }
+
+           /*
+            * Throwaway constraints must always be NO INHERIT; otherwise do
+            * what the catalog says.
+            */
+           tbinfo->notnull_noinh[j] = use_throwaway_notnull ||
+               PQgetvalue(res, r, i_notnull_noinherit)[0] == 't';
+
            tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions));
            tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation));
            tbinfo->attcompression[j] = *(PQgetvalue(res, r, i_attcompression));
            tbinfo->attrdefs[j] = NULL; /* fix below */
            if (PQgetvalue(res, r, i_atthasdef)[0] == 't')
                hasdefaults = true;
-           /* these flags will be set in flagInhAttrs() */
-           tbinfo->inhNotNull[j] = false;
        }
 
        if (hasdefaults)
                                     !tbinfo->attrdefs[j]->separate);
 
                    /*
-                    * Not Null constraint --- suppress if inherited, except
-                    * if partition, or in binary-upgrade case where that
-                    * won't work.
+                    * Not Null constraint --- suppress unless it is locally
+                    * defined, except if partition, or in binary-upgrade case
+                    * where that won't work.
                     */
-                   print_notnull = (tbinfo->notnull[j] &&
-                                    (!tbinfo->inhNotNull[j] ||
-                                     tbinfo->ispartition || dopt->binary_upgrade));
+                   print_notnull =
+                       (tbinfo->notnull_constrs[j] != NULL &&
+                        (!tbinfo->notnull_inh[j] || tbinfo->ispartition ||
+                         dopt->binary_upgrade));
 
                    /*
                     * Skip column if fully defined by reloftype, except in
 
 
                    if (print_notnull)
-                       appendPQExpBufferStr(q, " NOT NULL");
+                   {
+                       if (tbinfo->notnull_constrs[j][0] == '\0')
+                           appendPQExpBufferStr(q, " NOT NULL");
+                       else
+                           appendPQExpBuffer(q, " CONSTRAINT %s NOT NULL",
+                                             fmtId(tbinfo->notnull_constrs[j]));
+
+                       if (tbinfo->notnull_noinh[j])
+                           appendPQExpBufferStr(q, " NO INHERIT");
+                   }
 
                    /* Add collation if not default for the type */
                    if (OidIsValid(tbinfo->attcollation[j]))
                    appendPQExpBufferStr(q, "\n  AND attrelid = ");
                    appendStringLiteralAH(q, qualrelname, fout);
                    appendPQExpBufferStr(q, "::pg_catalog.regclass;\n");
+
+                   /*
+                    * If a not-null constraint comes from inheritance, reset
+                    * conislocal.  The inhcount is fixed later.
+                    */
+                   if (tbinfo->notnull_constrs[j] != NULL &&
+                       !tbinfo->notnull_throwaway[j] &&
+                       tbinfo->notnull_inh[j] &&
+                       !tbinfo->ispartition)
+                   {
+                       appendPQExpBufferStr(q, "UPDATE pg_catalog.pg_constraint\n"
+                                            "SET conislocal = false\n"
+                                            "WHERE contype = 'n' AND conrelid = ");
+                       appendStringLiteralAH(q, qualrelname, fout);
+                       appendPQExpBufferStr(q, "::pg_catalog.regclass AND\n"
+                                            "conname = ");
+                       appendStringLiteralAH(q, tbinfo->notnull_constrs[j], fout);
+                       appendPQExpBufferStr(q, ";\n");
+                   }
                }
            }
 
 
            /*
             * If we didn't dump the column definition explicitly above, and
-            * it is NOT NULL and did not inherit that property from a parent,
+            * it is not-null and did not inherit that property from a parent,
             * we have to mark it separately.
             */
            if (!shouldPrintColumn(dopt, tbinfo, j) &&
-               tbinfo->notnull[j] && !tbinfo->inhNotNull[j])
-               appendPQExpBuffer(q,
-                                 "ALTER %sTABLE ONLY %s ALTER COLUMN %s SET NOT NULL;\n",
-                                 foreign, qualrelname,
-                                 fmtId(tbinfo->attnames[j]));
+               tbinfo->notnull_constrs[j] != NULL &&
+               (!tbinfo->notnull_inh[j] && !tbinfo->ispartition && !dopt->binary_upgrade))
+           {
+               /* No constraint name desired? */
+               if (tbinfo->notnull_constrs[j][0] == '\0')
+                   appendPQExpBuffer(q,
+                                     "ALTER %sTABLE ONLY %s ALTER COLUMN %s SET NOT NULL;\n",
+                                     foreign, qualrelname,
+                                     fmtId(tbinfo->attnames[j]));
+               else
+                   appendPQExpBuffer(q,
+                                     "ALTER %sTABLE ONLY %s ADD CONSTRAINT %s NOT NULL %s;\n",
+                                     foreign, qualrelname,
+                                     tbinfo->notnull_constrs[j],
+                                     fmtId(tbinfo->attnames[j]));
+           }
 
            /*
             * Dump per-column statistics information. We only issue an ALTER
         * similar code in dumpIndex!
         */
 
+       /* Drop any not-null constraints that were added to support the PK */
+       if (coninfo->contype == 'p')
+           for (int i = 0; i < tbinfo->numatts; i++)
+               if (tbinfo->notnull_throwaway[i])
+                   appendPQExpBuffer(q, "\nALTER TABLE ONLY %s DROP CONSTRAINT %s;",
+                                     fmtQualifiedDumpable(tbinfo),
+                                     tbinfo->notnull_constrs[i]);
+
        /* If the index is clustered, we need to record that. */
        if (indxinfo->indisclustered)
        {
 
    char       *attcompression; /* per-attribute compression method */
    char      **attfdwoptions;  /* per-attribute fdw options */
    char      **attmissingval;  /* per attribute missing value */
-   bool       *notnull;        /* NOT NULL constraints on attributes */
-   bool       *inhNotNull;     /* true if NOT NULL is inherited */
+   char      **notnull_constrs;    /* NOT NULL constraint names. If null,
+                                    * there isn't one on this column. If
+                                    * empty string, unnamed constraint
+                                    * (pre-v17) */
+   bool       *notnull_noinh;  /* NOT NULL is NO INHERIT */
+   bool       *notnull_throwaway;  /* drop the NOT NULL constraint later */
+   bool       *notnull_inh;    /* true if NOT NULL has no local definition */
    struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
    struct _constraintInfo *checkexprs; /* CHECK constraints */
    bool        needs_override; /* has GENERATED ALWAYS AS IDENTITY */
 
                       );',
        regexp => qr/^
            \QCREATE TABLE dump_test.fk_reference_test_table (\E
-           \n\s+\Qcol1 integer NOT NULL\E
+           \n\s+\Qcol1 integer CONSTRAINT \E[a-z0-9_]*\Q NOT NULL NO INHERIT\E
            \n\);
            /xm,
        like =>
                        FOR VALUES FROM (\'2006-02-01\') TO (\'2006-03-01\');',
        regexp => qr/^
            \QCREATE TABLE dump_test_second_schema.measurement_y2006m2 (\E\n
-           \s+\Qcity_id integer DEFAULT nextval('dump_test.measurement_city_id_seq'::regclass) NOT NULL,\E\n
-           \s+\Qlogdate date NOT NULL,\E\n
+           \s+\Qcity_id integer DEFAULT nextval('dump_test.measurement_city_id_seq'::regclass) CONSTRAINT measurement_city_id_not_null NOT NULL,\E\n
+           \s+\Qlogdate date CONSTRAINT measurement_logdate_not_null NOT NULL,\E\n
            \s+\Qpeaktemp integer,\E\n
            \s+\Qunitsales integer DEFAULT 0,\E\n
            \s+\QCONSTRAINT measurement_peaktemp_check CHECK ((peaktemp >= '-460'::integer)),\E\n
                       );',
        regexp => qr/^
            \QCREATE TABLE dump_test.test_table_generated (\E\n
-           \s+\Qcol1 integer NOT NULL,\E\n
+           \s+\Qcol1 integer CONSTRAINT \E[a-z0-9_]*\Q NOT NULL NO INHERIT,\E\n
            \s+\Qcol2 integer GENERATED ALWAYS AS ((col1 * 2)) STORED\E\n
            \);
            /xms,
                        ) INHERITS (dump_test.test_inheritance_parent);',
        regexp => qr/^
        \QCREATE TABLE dump_test.test_inheritance_child (\E\n
-       \s+\Qcol1 integer,\E\n
+       \s+\Qcol1 integer NOT NULL,\E\n
        \s+\QCONSTRAINT test_inheritance_child CHECK ((col2 >= 142857))\E\n
        \)\n
        \QINHERITS (dump_test.test_inheritance_parent);\E\n
 
            }
            PQclear(result);
        }
+
+       /* If verbose, print NOT NULL constraints */
+       if (verbose)
+       {
+           printfPQExpBuffer(&buf,
+                             "SELECT co.conname, at.attname, co.connoinherit, co.conislocal,\n"
+                             "co.coninhcount <> 0\n"
+                             "FROM pg_catalog.pg_constraint co JOIN\n"
+                             "pg_catalog.pg_attribute at ON\n"
+                             "(at.attnum = co.conkey[1])\n"
+                             "WHERE co.contype = 'n' AND\n"
+                             "co.conrelid = '%s'::pg_catalog.regclass AND\n"
+                             "at.attrelid = '%s'::pg_catalog.regclass\n"
+                             "ORDER BY at.attnum",
+                             oid,
+                             oid);
+
+           result = PSQLexec(buf.data);
+           if (!result)
+               goto error_return;
+           else
+               tuples = PQntuples(result);
+
+           if (tuples > 0)
+               printTableAddFooter(&cont, _("Not-null constraints:"));
+
+           /* Might be an empty set - that's ok */
+           for (i = 0; i < tuples; i++)
+           {
+               bool        islocal = PQgetvalue(result, i, 3)[0] == 't';
+               bool        inherited = PQgetvalue(result, i, 4)[0] == 't';
+
+               printfPQExpBuffer(&buf, "    \"%s\" NOT NULL \"%s\"%s",
+                                 PQgetvalue(result, i, 0),
+                                 PQgetvalue(result, i, 1),
+                                 PQgetvalue(result, i, 2)[0] == 't' ?
+                                 " NO INHERIT" :
+                                 islocal && inherited ? _(" (local, inherited)") :
+                                 inherited ? _(" (inherited)") : "");
+
+               printTableAddFooter(&cont, buf.data);
+           }
+           PQclear(result);
+       }
    }
 
    /* Get view_def if table is a view or materialized view */
 
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 202308241
+#define CATALOG_VERSION_NO 202308251
 
 #endif
 
 
 typedef struct CookedConstraint
 {
-   ConstrType  contype;        /* CONSTR_DEFAULT or CONSTR_CHECK */
+   ConstrType  contype;        /* CONSTR_DEFAULT, CONSTR_CHECK,
+                                * CONSTR_NOTNULL */
    Oid         conoid;         /* constr OID if created, otherwise Invalid */
    char       *name;           /* name, or NULL if none */
-   AttrNumber  attnum;         /* which attr (only for DEFAULT) */
+   AttrNumber  attnum;         /* which attr (only for NOTNULL, DEFAULT) */
    Node       *expr;           /* transformed default or check expr */
    bool        skip_validation;    /* skip validation? (only for CHECK) */
    bool        is_local;       /* constraint has local (non-inherited) def */
                                       bool is_local,
                                       bool is_internal,
                                       const char *queryString);
+extern List *AddRelationNotNullConstraints(Relation rel,
+                                          List *constraints,
+                                          List *additional_notnulls);
 
 extern void RelationClearMissing(Relation rel);
 extern void SetAttrMissing(Oid relid, char *attname, char *value);
 
 /* Valid values for contype */
 #define CONSTRAINT_CHECK           'c'
 #define CONSTRAINT_FOREIGN         'f'
+#define CONSTRAINT_NOTNULL         'n'
 #define CONSTRAINT_PRIMARY         'p'
 #define CONSTRAINT_UNIQUE          'u'
 #define CONSTRAINT_TRIGGER         't'
                                  bool conNoInherit,
                                  bool is_internal);
 
-extern void RemoveConstraintById(Oid conId);
-extern void RenameConstraintById(Oid conId, const char *newname);
-
 extern bool ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
                                 const char *conname);
 extern bool ConstraintNameExists(const char *conname, Oid namespaceid);
                                  const char *label, Oid namespaceid,
                                  List *others);
 
+extern HeapTuple findNotNullConstraintAttnum(Oid relid, AttrNumber attnum);
+extern HeapTuple findNotNullConstraint(Oid relid, const char *colname);
+extern AttrNumber extractNotNullColumn(HeapTuple constrTup);
+extern bool AdjustNotNullInheritance1(Oid relid, AttrNumber attnum, int count);
+extern void AdjustNotNullInheritance(Oid relid, Bitmapset *columns, int count);
+extern List *RelationGetNotNullConstraints(Oid relid, bool cooked);
+
+extern void RemoveConstraintById(Oid conId);
+extern void RenameConstraintById(Oid conId, const char *newname);
+
 extern void AlterConstraintNamespaces(Oid ownerId, Oid oldNspId,
                                      Oid newNspId, bool isType, ObjectAddresses *objsMoved);
 extern void ConstraintSetParentConstraint(Oid childConstrId,
 
    AT_CookedColumnDefault,     /* add a pre-cooked column default */
    AT_DropNotNull,             /* alter column drop not null */
    AT_SetNotNull,              /* alter column set not null */
+   AT_SetAttNotNull,           /* set attnotnull w/o a constraint */
    AT_DropExpression,          /* alter column drop expression */
-   AT_CheckNotNull,            /* check column is already marked not null */
    AT_SetStatistics,           /* alter column set statistics */
    AT_SetOptions,              /* alter column set ( options ) */
    AT_ResetOptions,            /* alter column reset ( options ) */
  *     Create Table Statement
  *
  * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are
- * intermixed in tableElts, and constraints is NIL.  After parse analysis,
- * tableElts contains just ColumnDefs, and constraints contains just
- * Constraint nodes (in fact, only CONSTR_CHECK nodes, in the present
- * implementation).
+ * intermixed in tableElts, and constraints and nnconstraints are NIL.  After
+ * parse analysis, tableElts contains just ColumnDefs, nnconstraints contains
+ * Constraint nodes of CONSTR_NOTNULL type from various sources, and
+ * constraints contains just CONSTR_CHECK Constraint nodes.
  * ----------------------
  */
 
    PartitionSpec *partspec;    /* PARTITION BY clause */
    TypeName   *ofTypename;     /* OF typename */
    List       *constraints;    /* constraints (list of Constraint nodes) */
+   List       *nnconstraints;  /* NOT NULL constraints (ditto) */
    List       *options;        /* options from WITH clause */
    OnCommitAction oncommit;    /* what do we do at COMMIT? */
    char       *tablespacename; /* table space to use, or NULL */
    char       *cooked_expr;    /* expr, as nodeToString representation */
    char        generated_when; /* ALWAYS or BY DEFAULT */
 
+   /* Fields used for "raw" NOT NULL constraints: */
+   int         inhcount;       /* initial inheritance count to apply */
+
    /* Fields used for unique constraints (UNIQUE and PRIMARY KEY): */
    bool        nulls_not_distinct; /* null treatment for UNIQUE constraints */
    List       *keys;           /* String nodes naming referenced key
-                                * column(s) */
+                                * column(s); also used for NOT NULL */
    List       *including;      /* String nodes naming referenced nonkey
                                 * column(s) */
 
 
 NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
 NOTICE:    subcommand: type ADD COLUMN (and recurse) desc column b of table parent
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint parent_b_not_null on table parent
 NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
 ALTER TABLE parent RENAME COLUMN b TO c;
 NOTICE:  DDL test: type simple, tag ALTER TABLE
 DROP TABLE part2;
 ALTER TABLE part ADD PRIMARY KEY (a);
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
-NOTICE:    subcommand: type SET NOT NULL desc column a of table part
-NOTICE:    subcommand: type SET NOT NULL desc column a of table part1
+NOTICE:    subcommand: type SET ATTNOTNULL desc column a of table part
+NOTICE:    subcommand: type SET ATTNOTNULL desc column a of table part1
 NOTICE:    subcommand: type ADD INDEX desc index part_pkey
 ALTER TABLE parent ALTER COLUMN a SET NOT NULL;
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
-NOTICE:    subcommand: type SET NOT NULL desc column a of table parent
-NOTICE:    subcommand: type SET NOT NULL desc column a of table child
-NOTICE:    subcommand: type SET NOT NULL desc column a of table grandchild
+NOTICE:    subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent
 ALTER TABLE parent ALTER COLUMN a DROP NOT NULL;
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
-NOTICE:    subcommand: type DROP NOT NULL desc column a of table parent
-NOTICE:    subcommand: type DROP NOT NULL desc column a of table child
-NOTICE:    subcommand: type DROP NOT NULL desc column a of table grandchild
+NOTICE:    subcommand: type DROP NOT NULL (and recurse) desc column a of table parent
 ALTER TABLE parent ALTER COLUMN a SET NOT NULL;
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
-NOTICE:    subcommand: type SET NOT NULL desc column a of table parent
-NOTICE:    subcommand: type SET NOT NULL desc column a of table child
-NOTICE:    subcommand: type SET NOT NULL desc column a of table grandchild
+NOTICE:    subcommand: type SET NOT NULL (and recurse) desc constraint parent_a_not_null on table parent
 ALTER TABLE parent ALTER COLUMN a ADD GENERATED ALWAYS AS IDENTITY;
 NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
 NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
 NOTICE:    subcommand: type ALTER COLUMN SET TYPE desc column c of table parent
 NOTICE:    subcommand: type ALTER COLUMN SET TYPE desc column c of table child
 NOTICE:    subcommand: type ALTER COLUMN SET TYPE desc column c of table grandchild
+NOTICE:    subcommand: type (re) ADD CONSTRAINT desc constraint parent_b_not_null on table parent
 NOTICE:    subcommand: type (re) ADD STATS desc statistics object parent_stat
 ALTER TABLE parent ALTER COLUMN c SET DEFAULT 0;
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
 
 NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
 NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
 NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type SET ATTNOTNULL desc <NULL>
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
     EXCLUDE USING btree (check_col_2 WITH =)
 );
 NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type SET ATTNOTNULL desc <NULL>
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
 );
 NOTICE:  DDL test: type simple, tag CREATE TABLE
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
-NOTICE:    subcommand: type SET NOT NULL desc column name of table employees
+NOTICE:    subcommand: type SET ATTNOTNULL desc column name of table employees
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 -- Inheritance
 CREATE TABLE person (
    location    point
 );
 NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type SET ATTNOTNULL desc <NULL>
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 CREATE TABLE emp (
    salary      int4,
   EXCLUDING ALL
 );
 NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint datatype_table_id_big_not_null on table like_datatype_table
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint datatype_table_id_not_null on table like_datatype_table
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint datatype_table_is_small_not_null on table like_datatype_table
 CREATE TABLE like_fkey_table (
   LIKE fkey_table
   INCLUDING DEFAULTS
 );
 NOTICE:  DDL test: type simple, tag CREATE TABLE
 NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type SET ATTNOTNULL desc column id of table like_fkey_table
 NOTICE:    subcommand: type ALTER COLUMN SET DEFAULT (precooked) desc column id of table like_fkey_table
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint fkey_table_big_id_not_null on table like_fkey_table
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint fkey_table_check_col_1_not_null on table like_fkey_table
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint fkey_table_check_col_2_not_null on table like_fkey_table
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint fkey_table_datatype_id_not_null on table like_fkey_table
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint fkey_table_id_not_null on table like_fkey_table
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 -- Volatile table types
     id INT PRIMARY KEY
 );
 NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type SET ATTNOTNULL desc <NULL>
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 CREATE TEMP TABLE temp_table (
     id INT PRIMARY KEY
 );
 NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type SET ATTNOTNULL desc <NULL>
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 CREATE TEMP TABLE temp_table_commit_delete (
     id INT PRIMARY KEY
 )
 ON COMMIT DELETE ROWS;
 NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type SET ATTNOTNULL desc <NULL>
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 CREATE TEMP TABLE temp_table_commit_drop (
     id INT PRIMARY KEY
 )
 ON COMMIT DROP;
 NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type SET ATTNOTNULL desc <NULL>
 NOTICE:  DDL test: type simple, tag CREATE INDEX
 
            case AT_SetNotNull:
                strtype = "SET NOT NULL";
                break;
+           case AT_SetAttNotNull:
+               strtype = "SET ATTNOTNULL";
+               break;
            case AT_DropExpression:
                strtype = "DROP EXPRESSION";
                break;
-           case AT_CheckNotNull:
-               strtype = "CHECK NOT NULL";
-               break;
            case AT_SetStatistics:
                strtype = "SET STATS";
                break;
 
 -- test checking for null values and primary key
 create table atacc1 (test int not null);
 alter table atacc1 add constraint "atacc1_pkey" primary key (test);
+\d atacc1
+               Table "public.atacc1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ test   | integer |           | not null | 
+Indexes:
+    "atacc1_pkey" PRIMARY KEY, btree (test)
+
 alter table atacc1 alter column test drop not null;
-ERROR:  column "test" is in a primary key
+\d atacc1
+               Table "public.atacc1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ test   | integer |           | not null | 
+Indexes:
+    "atacc1_pkey" PRIMARY KEY, btree (test)
+
 alter table atacc1 drop constraint "atacc1_pkey";
-alter table atacc1 alter column test drop not null;
+\d atacc1
+               Table "public.atacc1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ test   | integer |           |          | 
+
 insert into atacc1 values (null);
 alter table atacc1 alter test set not null;
 ERROR:  column "test" of relation "atacc1" contains null values
 ERROR:  column "a" of relation "parent" contains null values
 alter table child alter a set not null;
 ERROR:  column "a" of relation "child" contains null values
-delete from parent;
-alter table only parent alter a set not null;
-insert into parent values (NULL);
-ERROR:  null value in column "a" of relation "parent" violates not-null constraint
-DETAIL:  Failing row contains (null).
-alter table child alter a set not null;
-insert into child (a, b) values (NULL, 'foo');
-ERROR:  null value in column "a" of relation "child" violates not-null constraint
-DETAIL:  Failing row contains (null, foo).
-delete from child;
-alter table child alter a set not null;
-insert into child (a, b) values (NULL, 'foo');
-ERROR:  null value in column "a" of relation "child" violates not-null constraint
-DETAIL:  Failing row contains (null, foo).
 drop table child;
 drop table parent;
 -- test setting and removing default values
     TABLE "ataddindex" CONSTRAINT "ataddindex_ref_id_fkey" FOREIGN KEY (ref_id) REFERENCES ataddindex(id)
 
 DROP TABLE ataddindex;
+CREATE TABLE atnotnull1 ();
+ALTER TABLE atnotnull1
+  ADD COLUMN a INT,
+  ALTER a SET NOT NULL;
+ALTER TABLE atnotnull1
+  ADD COLUMN b INT,
+  ADD NOT NULL b;
+ALTER TABLE atnotnull1
+  ADD COLUMN c INT,
+  ADD PRIMARY KEY (c);
+\d+ atnotnull1
+                                Table "public.atnotnull1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+ c      | integer |           | not null |         | plain   |              | 
+Indexes:
+    "atnotnull1_pkey" PRIMARY KEY, btree (c)
+Not-null constraints:
+    "atnotnull1_a_not_null" NOT NULL "a"
+    "atnotnull1_b_not_null" NOT NULL "b"
+
 -- cannot drop column that is part of the partition key
 CREATE TABLE partitioned (
    a int,
 -- partitions exist
 ALTER TABLE ONLY list_parted2 ALTER b SET NOT NULL;
 ERROR:  constraint must be added to child tables too
-DETAIL:  Column "b" of relation "part_2" is not already NOT NULL.
 HINT:  Do not specify the ONLY keyword.
 ALTER TABLE ONLY list_parted2 ADD CONSTRAINT check_b CHECK (b <> 'zz');
 ERROR:  constraint must be added to child tables too
 
 DETAIL:  Key (b)=(1111) is not present in table "clstr_tst_s".
 SELECT conname FROM pg_constraint WHERE conrelid = 'clstr_tst'::regclass
 ORDER BY 1;
-    conname     
-----------------
+       conname        
+----------------------
+ clstr_tst_a_not_null
  clstr_tst_con
  clstr_tst_pkey
-(2 rows)
+(3 rows)
 
 SELECT relname, relkind,
     EXISTS(SELECT 1 FROM pg_class WHERE oid = c.reltoastrelid) AS hastoast
 
 DETAIL:  Failing row contains (null, 3).
 DROP TABLE ATACC1 CASCADE;
 NOTICE:  drop cascades to table atacc2
+-- NOT NULL NO INHERIT
+CREATE TABLE ATACC1 (a int, not null a no inherit);
+CREATE TABLE ATACC2 () INHERITS (ATACC1);
+\d+ ATACC2
+                                  Table "public.atacc2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Inherits: atacc1
+
+DROP TABLE ATACC1, ATACC2;
+CREATE TABLE ATACC1 (a int);
+ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT;
+CREATE TABLE ATACC2 () INHERITS (ATACC1);
+\d+ ATACC2
+                                  Table "public.atacc2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Inherits: atacc1
+
+DROP TABLE ATACC1, ATACC2;
+CREATE TABLE ATACC1 (a int);
+CREATE TABLE ATACC2 () INHERITS (ATACC1);
+ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT;
+\d+ ATACC2
+                                  Table "public.atacc2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Inherits: atacc1
+
+DROP TABLE ATACC1, ATACC2;
 --
 -- Check constraints on INSERT INTO
 --
 ERROR:  could not create exclusion constraint "deferred_excl_f1_excl"
 DETAIL:  Key (f1)=(3) conflicts with key (f1)=(3).
 DROP TABLE deferred_excl;
+-- verify constraints created for NOT NULL clauses
+CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL);
+\d+ notnull_tbl1
+                               Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "notnull_tbl1_a_not_null" NOT NULL "a"
+
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+         conname         | contype | conkey 
+-------------------------+---------+--------
+ notnull_tbl1_a_not_null | n       | {1}
+(1 row)
+
+-- no-op
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+\d+ notnull_tbl1
+                               Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "notnull_tbl1_a_not_null" NOT NULL "a"
+
+-- duplicate name
+ALTER TABLE notnull_tbl1 ADD COLUMN b INT CONSTRAINT notnull_tbl1_a_not_null NOT NULL;
+ERROR:  constraint "notnull_tbl1_a_not_null" for relation "notnull_tbl1" already exists
+-- DROP NOT NULL gets rid of both the attnotnull flag and the constraint itself
+ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL;
+\d notnull_tbl1
+            Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+ conname | contype | conkey 
+---------+---------+--------
+(0 rows)
+
+-- SET NOT NULL puts both back
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+\d notnull_tbl1
+            Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           | not null | 
+
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+         conname         | contype | conkey 
+-------------------------+---------+--------
+ notnull_tbl1_a_not_null | n       | {1}
+(1 row)
+
+-- Doing it twice doesn't create a redundant constraint
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+         conname         | contype | conkey 
+-------------------------+---------+--------
+ notnull_tbl1_a_not_null | n       | {1}
+(1 row)
+
+-- Using the "table constraint" syntax also works
+ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
+\d notnull_tbl1
+            Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           | not null | 
+
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+ conname | contype | conkey 
+---------+---------+--------
+ foobar  | n       | {1}
+(1 row)
+
+DROP TABLE notnull_tbl1;
+-- nope
+CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL);
+ERROR:  constraint "blah" for relation "notnull_tbl2" already exists
+CREATE TABLE notnull_tbl2 (a INTEGER PRIMARY KEY);
+ALTER TABLE notnull_tbl2 ALTER a DROP NOT NULL;
+ERROR:  column "a" is in a primary key
+CREATE TABLE notnull_tbl3 (a INTEGER NOT NULL, CHECK (a IS NOT NULL));
+ALTER TABLE notnull_tbl3 ALTER A DROP NOT NULL;
+ALTER TABLE notnull_tbl3 ADD b int, ADD CONSTRAINT pk PRIMARY KEY (a, b);
+\d notnull_tbl3
+            Table "public.notnull_tbl3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           | not null | 
+ b      | integer |           | not null | 
+Indexes:
+    "pk" PRIMARY KEY, btree (a, b)
+Check constraints:
+    "notnull_tbl3_a_check" CHECK (a IS NOT NULL)
+
+ALTER TABLE notnull_tbl3 DROP CONSTRAINT pk;
+\d notnull_tbl3
+            Table "public.notnull_tbl3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+Check constraints:
+    "notnull_tbl3_a_check" CHECK (a IS NOT NULL)
+
+-- Primary keys in parent table cause NOT NULL constraint to spawn on their
+-- children.  Verify that they work correctly.
+CREATE TABLE cnn_parent (a int, b int);
+CREATE TABLE cnn_child () INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild (NOT NULL b) INHERITS (cnn_child);
+CREATE TABLE cnn_child2 (NOT NULL a NO INHERIT) INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild2 () INHERITS (cnn_grandchild, cnn_child2);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ALTER TABLE cnn_parent ADD PRIMARY KEY (b);
+\d+ cnn_grandchild
+                              Table "public.cnn_grandchild"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "cnn_grandchild_b_not_null" NOT NULL "b" (local, inherited)
+Inherits: cnn_child
+Child tables: cnn_grandchild2
+
+\d+ cnn_grandchild2
+                              Table "public.cnn_grandchild2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "cnn_grandchild_b_not_null" NOT NULL "b" (inherited)
+Inherits: cnn_grandchild,
+          cnn_child2
+
+ALTER TABLE cnn_parent DROP CONSTRAINT cnn_parent_pkey;
+\set VERBOSITY terse
+DROP TABLE cnn_parent CASCADE;
+NOTICE:  drop cascades to 4 other objects
+\set VERBOSITY default
+-- As above, but create the primary key ahead of time
+CREATE TABLE cnn_parent (a int, b int PRIMARY KEY);
+CREATE TABLE cnn_child () INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild (NOT NULL b) INHERITS (cnn_child);
+CREATE TABLE cnn_child2 (NOT NULL a NO INHERIT) INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild2 () INHERITS (cnn_grandchild, cnn_child2);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ALTER TABLE cnn_parent ADD PRIMARY KEY (b);
+ERROR:  multiple primary keys for table "cnn_parent" are not allowed
+\d+ cnn_grandchild
+                              Table "public.cnn_grandchild"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "cnn_grandchild_b_not_null" NOT NULL "b" (local, inherited)
+Inherits: cnn_child
+Child tables: cnn_grandchild2
+
+\d+ cnn_grandchild2
+                              Table "public.cnn_grandchild2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "cnn_grandchild_b_not_null" NOT NULL "b" (inherited)
+Inherits: cnn_grandchild,
+          cnn_child2
+
+ALTER TABLE cnn_parent DROP CONSTRAINT cnn_parent_pkey;
+\set VERBOSITY terse
+DROP TABLE cnn_parent CASCADE;
+NOTICE:  drop cascades to 4 other objects
+\set VERBOSITY default
+-- As above, but create the primary key using a UNIQUE index
+CREATE TABLE cnn_parent (a int, b int);
+CREATE TABLE cnn_child () INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild (NOT NULL b) INHERITS (cnn_child);
+CREATE TABLE cnn_child2 (NOT NULL a NO INHERIT) INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild2 () INHERITS (cnn_grandchild, cnn_child2);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+CREATE UNIQUE INDEX b_uq ON cnn_parent (b);
+ALTER TABLE cnn_parent ADD PRIMARY KEY USING INDEX b_uq;
+\d+ cnn_grandchild
+                              Table "public.cnn_grandchild"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "cnn_grandchild_b_not_null" NOT NULL "b" (local, inherited)
+Inherits: cnn_child
+Child tables: cnn_grandchild2
+
+\d+ cnn_grandchild2
+                              Table "public.cnn_grandchild2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "cnn_grandchild_b_not_null" NOT NULL "b" (inherited)
+Inherits: cnn_grandchild,
+          cnn_child2
+
+ALTER TABLE cnn_parent DROP CONSTRAINT cnn_parent_pkey;
+ERROR:  constraint "cnn_parent_pkey" of relation "cnn_parent" does not exist
+-- keeps these tables around, for pg_upgrade testing
 -- Comments
 -- Setup a low-level role to enforce non-superuser checks.
 CREATE ROLE regress_constraint_comments;
 
 ) FOR VALUES IN ('b');
 NOTICE:  merging constraint "check_a" with inherited definition
 -- conislocal should be false for any merged constraints, true otherwise
-SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY conislocal, coninhcount;
- conislocal | coninhcount 
-------------+-------------
- f          |           1
- t          |           0
-(2 rows)
+SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname;
+      conname      | conislocal | coninhcount 
+-------------------+------------+-------------
+ check_a           | f          |           1
+ part_b_b_not_null | t          |           1
+ check_b           | t          |           0
+(3 rows)
 
 -- Once check_b is added to the parent, it should be made non-local for part_b
 ALTER TABLE parted ADD CONSTRAINT check_b CHECK (b >= 0);
 NOTICE:  merging constraint "check_b" with inherited definition
-SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass;
- conislocal | coninhcount 
-------------+-------------
- f          |           1
- f          |           1
-(2 rows)
+SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname;
+      conname      | conislocal | coninhcount 
+-------------------+------------+-------------
+ check_a           | f          |           1
+ check_b           | f          |           1
+ part_b_b_not_null | t          |           1
+(3 rows)
 
 -- Neither check_a nor check_b are droppable from part_b
 ALTER TABLE part_b DROP CONSTRAINT check_a;
 -- traditional inheritance where they will be left behind, because they would
 -- be local constraints.
 ALTER TABLE parted DROP CONSTRAINT check_a, DROP CONSTRAINT check_b;
-SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass;
- conislocal | coninhcount 
-------------+-------------
-(0 rows)
+SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname;
+      conname      | conislocal | coninhcount 
+-------------------+------------+-------------
+ part_b_b_not_null | t          |           1
+(1 row)
 
 -- specify PARTITION BY for a partition
 CREATE TABLE fail_part_col_not_found PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE (c);
  b      | integer |           | not null | 1       | plain    |              | 
 Partition of: parted FOR VALUES IN ('b')
 Partition constraint: ((a IS NOT NULL) AND (a = 'b'::text))
+Not-null constraints:
+    "part_b_b_not_null" NOT NULL "b" (local, inherited)
 
 -- Both partition bound and partition key in describe output
 \d+ part_c
 Partition of: parted FOR VALUES IN ('c')
 Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text))
 Partition key: RANGE (b)
+Not-null constraints:
+    "part_c_b_not_null" NOT NULL "b" (local, inherited)
 Partitions: part_c_1_10 FOR VALUES FROM (1) TO (10)
 
 -- a level-2 partition's constraint will include the parent's expressions
  b      | integer |           | not null | 0       | plain    |              | 
 Partition of: part_c FOR VALUES FROM (1) TO (10)
 Partition constraint: ((a IS NOT NULL) AND (a = 'c'::text) AND (b IS NOT NULL) AND (b >= 1) AND (b < 10))
+Not-null constraints:
+    "part_c_b_not_null" NOT NULL "b" (inherited)
 
 -- Show partition count in the parent's describe output
 -- Tempted to include \d+ output listing partitions with bound info but
 
  a      | text |           | not null |         | main     |              | 
  b      | text |           |          |         | extended |              | 
  c      | text |           |          |         | external |              | 
+Not-null constraints:
+    "ctlt12_storage_a_not_null" NOT NULL "a"
 
 CREATE TABLE ctlt12_comments (LIKE ctlt1 INCLUDING COMMENTS, LIKE ctlt2 INCLUDING COMMENTS);
 \d+ ctlt12_comments
  a      | text |           | not null |         | extended |              | A
  b      | text |           |          |         | extended |              | B
  c      | text |           |          |         | extended |              | C
+Not-null constraints:
+    "ctlt12_comments_a_not_null" NOT NULL "a"
 
 CREATE TABLE ctlt1_inh (LIKE ctlt1 INCLUDING CONSTRAINTS INCLUDING COMMENTS) INHERITS (ctlt1);
 NOTICE:  merging column "a" with inherited definition
  b      | text |           |          |         | extended |              | B
 Check constraints:
     "ctlt1_a_check" CHECK (length(a) > 2)
+Not-null constraints:
+    "ctlt1_inh_a_not_null" NOT NULL "a" (local, inherited)
 Inherits: ctlt1
 
 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt1_inh'::regclass;
     "ctlt1_a_check" CHECK (length(a) > 2)
     "ctlt3_a_check" CHECK (length(a) < 5)
     "ctlt3_c_check" CHECK (length(c) < 7)
+Not-null constraints:
+    "ctlt13_inh_a_not_null" NOT NULL "a" (inherited)
 Inherits: ctlt1,
           ctlt3
 
     "ctlt1_a_check" CHECK (length(a) > 2)
     "ctlt3_a_check" CHECK (length(a) < 5)
     "ctlt3_c_check" CHECK (length(c) < 7)
+Not-null constraints:
+    "ctlt13_like_a_not_null" NOT NULL "a" (inherited)
 Inherits: ctlt1
 
 SELECT description FROM pg_description, pg_constraint c WHERE classoid = 'pg_constraint'::regclass AND objoid = c.oid AND c.conrelid = 'ctlt13_like'::regclass;
 
 NOTICE:  END: command_tag=CREATE SEQUENCE type=sequence identity=evttrig.one_col_a_seq
 NOTICE:  END: command_tag=CREATE SEQUENCE type=sequence identity=evttrig.one_col_c_seq
 NOTICE:  END: command_tag=CREATE TABLE type=table identity=evttrig.one
+NOTICE:  END: command_tag=ALTER TABLE type=table identity=evttrig.one
 NOTICE:  END: command_tag=CREATE INDEX type=index identity=evttrig.one_pkey
 NOTICE:  END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.one_col_a_seq
 NOTICE:  END: command_tag=ALTER SEQUENCE type=sequence identity=evttrig.one_col_c_seq
     id int PRIMARY KEY)
     PARTITION BY RANGE (id);
 NOTICE:  END: command_tag=CREATE TABLE type=table identity=evttrig.parted
+NOTICE:  END: command_tag=ALTER TABLE type=table identity=evttrig.parted
 NOTICE:  END: command_tag=CREATE INDEX type=index identity=evttrig.parted_pkey
 CREATE TABLE evttrig.part_1_10 PARTITION OF evttrig.parted (id)
   FOR VALUES FROM (1) TO (10);
 
 Check constraints:
     "ft1_c2_check" CHECK (c2 <> ''::text)
     "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date)
+Not-null constraints:
+    "ft1_c1_not_null" NOT NULL "c1"
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
 Check constraints:
     "ft1_c2_check" CHECK (c2 <> ''::text)
     "ft1_c3_check" CHECK (c3 >= '01-01-1994'::date AND c3 <= '01-31-1994'::date)
+Not-null constraints:
+    "ft1_c1_not_null" NOT NULL "c1"
+    "ft1_c6_not_null" NOT NULL "c6"
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
  c1     | integer |           | not null |         | plain    |              | 
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
  c1     | integer |           | not null |         |             | plain    |              | 
  c2     | text    |           |          |         |             | extended |              | 
  c3     | date    |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1" (inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  c1     | integer |           | not null |         | plain    |              | 
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
 
 CREATE FOREIGN TABLE ft2 (
    c1 integer NOT NULL,
  c1     | integer |           | not null |         |             | plain    |              | 
  c2     | text    |           |          |         |             | extended |              | 
  c3     | date    |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1"
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
  c1     | integer |           | not null |         | plain    |              | 
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
  c1     | integer |           | not null |         |             | plain    |              | 
  c2     | text    |           |          |         |             | extended |              | 
  c3     | date    |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  c1     | integer |           | not null |         |             | plain    |              | 
  c2     | text    |           |          |         |             | extended |              | 
  c3     | date    |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  c1     | integer |           | not null |         | plain    |              | 
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (inherited)
 Inherits: ft2
 
 \d+ ft3
  c1     | integer |           | not null |         |             | plain    |              | 
  c2     | text    |           |          |         |             | extended |              | 
  c3     | date    |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "ft3_c1_not_null" NOT NULL "c1" (local, inherited)
 Server: s0
 Inherits: ft2
 
  c6     | integer |           |          |         | plain    |              | 
  c7     | integer |           | not null |         | plain    |              | 
  c8     | integer |           |          |         | plain    |              | 
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
+    "fd_pt1_c7_not_null" NOT NULL "c7"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
  c6     | integer |           |          |         |             | plain    |              | 
  c7     | integer |           | not null |         |             | plain    |              | 
  c8     | integer |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
+    "fd_pt1_c7_not_null" NOT NULL "c7" (inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  c6     | integer |           |          |         | plain    |              | 
  c7     | integer |           | not null |         | plain    |              | 
  c8     | integer |           |          |         | plain    |              | 
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (inherited)
+    "fd_pt1_c7_not_null" NOT NULL "c7" (inherited)
 Inherits: ft2
 
 \d+ ft3
  c6     | integer |           |          |         |             | plain    |              | 
  c7     | integer |           | not null |         |             | plain    |              | 
  c8     | integer |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "ft3_c1_not_null" NOT NULL "c1" (local, inherited)
+    "fd_pt1_c7_not_null" NOT NULL "c7" (inherited)
 Server: s0
 Inherits: ft2
 
  c6     | integer |           | not null |         | plain    |              | 
  c7     | integer |           |          |         | plain    |              | 
  c8     | text    |           |          |         | external |              | 
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
+    "fd_pt1_c6_not_null" NOT NULL "c6"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
  c6     | integer |           | not null |         |             | plain    |              | 
  c7     | integer |           |          |         |             | plain    |              | 
  c8     | text    |           |          |         |             | external |              | 
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
+    "fd_pt1_c6_not_null" NOT NULL "c6" (inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  c1     | integer |           | not null |         | plain    | 10000        | 
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
  c1     | integer |           | not null |         |             | plain    | 10000        | 
  c2     | text    |           |          |         |             | extended |              | 
  c3     | date    |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
   FROM pg_class AS pc JOIN pg_constraint AS pgc ON (conrelid = pc.oid)
   WHERE pc.relname = 'fd_pt1'
   ORDER BY 1,2;
- relname |  conname   | contype | conislocal | coninhcount | connoinherit 
----------+------------+---------+------------+-------------+--------------
- fd_pt1  | fd_pt1chk1 | c       | t          |           0 | t
- fd_pt1  | fd_pt1chk2 | c       | t          |           0 | f
-(2 rows)
+ relname |      conname       | contype | conislocal | coninhcount | connoinherit 
+---------+--------------------+---------+------------+-------------+--------------
+ fd_pt1  | fd_pt1_c1_not_null | n       | t          |           0 | f
+ fd_pt1  | fd_pt1chk1         | c       | t          |           0 | t
+ fd_pt1  | fd_pt1chk2         | c       | t          |           0 | f
+(3 rows)
 
 -- child does not inherit NO INHERIT constraints
 \d+ fd_pt1
 Check constraints:
     "fd_pt1chk1" CHECK (c1 > 0) NO INHERIT
     "fd_pt1chk2" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
  c3     | date    |           |          |         |             | plain    |              | 
 Check constraints:
     "fd_pt1chk2" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
 Check constraints:
     "fd_pt1chk1" CHECK (c1 > 0) NO INHERIT
     "fd_pt1chk2" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
  c3     | date    |           |          |         |             | plain    |              | 
 Check constraints:
     "fd_pt1chk2" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  c3     | date    |           |          |         | plain    |              | 
 Check constraints:
     "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
 Check constraints:
     "fd_pt1chk2" CHECK (c2 <> ''::text)
     "fd_pt1chk3" CHECK (c2 <> ''::text) NOT VALID
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  c3     | date    |           |          |         | plain    |              | 
 Check constraints:
     "fd_pt1chk3" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "c1"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
 Check constraints:
     "fd_pt1chk2" CHECK (c2 <> ''::text)
     "fd_pt1chk3" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "c1" (local, inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  f3     | date    |           |          |         | plain    |              | 
 Check constraints:
     "f2_check" CHECK (f2 <> ''::text)
+Not-null constraints:
+    "fd_pt1_c1_not_null" NOT NULL "f1"
 Child tables: ft2, FOREIGN
 
 \d+ ft2
 Check constraints:
     "f2_check" CHECK (f2 <> ''::text)
     "fd_pt1chk2" CHECK (f2 <> ''::text)
+Not-null constraints:
+    "ft2_c1_not_null" NOT NULL "f1" (local, inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 Inherits: fd_pt1
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
 Partition key: LIST (c1)
+Not-null constraints:
+    "fd_pt2_c1_not_null" NOT NULL "c1"
 Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN
 
 \d+ fd_pt2_1
  c3     | date    |           |          |         |             | plain    |              | 
 Partition of: fd_pt2 FOR VALUES IN (1)
 Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1))
+Not-null constraints:
+    "fd_pt2_c1_not_null" NOT NULL "c1" (inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
  c2     | text         |           |          |         |             | extended |              | 
  c3     | date         |           |          |         |             | plain    |              | 
  c4     | character(1) |           |          |         |             | extended |              | 
+Not-null constraints:
+    "fd_pt2_1_c1_not_null" NOT NULL "c1"
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
 Partition key: LIST (c1)
+Not-null constraints:
+    "fd_pt2_c1_not_null" NOT NULL "c1"
 Number of partitions: 0
 
 CREATE FOREIGN TABLE fd_pt2_1 (
  c1     | integer |           | not null |         |             | plain    |              | 
  c2     | text    |           |          |         |             | extended |              | 
  c3     | date    |           |          |         |             | plain    |              | 
+Not-null constraints:
+    "fd_pt2_1_c1_not_null" NOT NULL "c1"
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
 Partition key: LIST (c1)
+Not-null constraints:
+    "fd_pt2_c1_not_null" NOT NULL "c1"
 Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN
 
 \d+ fd_pt2_1
  c3     | date    |           |          |         |             | plain    |              | 
 Partition of: fd_pt2 FOR VALUES IN (1)
 Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1))
+Not-null constraints:
+    "fd_pt2_1_c1_not_null" NOT NULL "c1" (inherited)
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
  c2     | text    |           |          |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
 Partition key: LIST (c1)
+Not-null constraints:
+    "fd_pt2_c1_not_null" NOT NULL "c1"
 Partitions: fd_pt2_1 FOR VALUES IN (1), FOREIGN
 
 \d+ fd_pt2_1
 Partition constraint: ((c1 IS NOT NULL) AND (c1 = 1))
 Check constraints:
     "p21chk" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "fd_pt2_1_c1_not_null" NOT NULL "c1" (inherited)
+    "fd_pt2_1_c3_not_null" NOT NULL "c3"
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
  c2     | text    |           | not null |         | extended |              | 
  c3     | date    |           |          |         | plain    |              | 
 Partition key: LIST (c1)
+Not-null constraints:
+    "fd_pt2_c1_not_null" NOT NULL "c1"
+    "fd_pt2_c2_not_null" NOT NULL "c2"
 Number of partitions: 0
 
 \d+ fd_pt2_1
  c3     | date    |           | not null |         |             | plain    |              | 
 Check constraints:
     "p21chk" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "fd_pt2_1_c1_not_null" NOT NULL "c1"
+    "fd_pt2_1_c3_not_null" NOT NULL "c3"
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
 Partition key: LIST (c1)
 Check constraints:
     "fd_pt2chk1" CHECK (c1 > 0)
+Not-null constraints:
+    "fd_pt2_c1_not_null" NOT NULL "c1"
+    "fd_pt2_c2_not_null" NOT NULL "c2"
 Number of partitions: 0
 
 \d+ fd_pt2_1
  c3     | date    |           | not null |         |             | plain    |              | 
 Check constraints:
     "p21chk" CHECK (c2 <> ''::text)
+Not-null constraints:
+    "fd_pt2_1_c1_not_null" NOT NULL "c1"
+    "fd_pt2_1_c2_not_null" NOT NULL "c2"
+    "fd_pt2_1_c3_not_null" NOT NULL "c3"
 Server: s0
 FDW options: (delimiter ',', quote '"', "be quoted" 'value')
 
 
  part33_self_fk | parted_self_fk_id_abc_fkey | f       | t            | parted_self_fk_id_abc_fkey | t            | parted_self_fk
  part3_self_fk  | parted_self_fk_id_abc_fkey | f       | t            | parted_self_fk_id_abc_fkey | t            | parted_self_fk
  parted_self_fk | parted_self_fk_id_abc_fkey | f       | t            |                            |              | parted_self_fk
+ part1_self_fk  | part1_self_fk_id_not_null  | n       | t            |                            |              | 
+ part2_self_fk  | parted_self_fk_id_not_null | n       | t            |                            |              | 
+ part32_self_fk | part3_self_fk_id_not_null  | n       | t            |                            |              | 
+ part33_self_fk | part33_self_fk_id_not_null | n       | t            |                            |              | 
+ part3_self_fk  | part3_self_fk_id_not_null  | n       | t            |                            |              | 
+ parted_self_fk | parted_self_fk_id_not_null | n       | t            |                            |              | 
  part1_self_fk  | part1_self_fk_pkey         | p       | t            | parted_self_fk_pkey        | t            | 
  part2_self_fk  | part2_self_fk_pkey         | p       | t            | parted_self_fk_pkey        | t            | 
  part32_self_fk | part32_self_fk_pkey        | p       | t            | part3_self_fk_pkey         | t            | 
  part33_self_fk | part33_self_fk_pkey        | p       | t            | part3_self_fk_pkey         | t            | 
  part3_self_fk  | part3_self_fk_pkey         | p       | t            | parted_self_fk_pkey        | t            | 
  parted_self_fk | parted_self_fk_pkey        | p       | t            |                            |              | 
-(12 rows)
+(18 rows)
 
 -- detach and re-attach multiple times just to ensure everything is kosher
 ALTER TABLE parted_self_fk DETACH PARTITION part2_self_fk;
  part33_self_fk | parted_self_fk_id_abc_fkey | f       | t            | parted_self_fk_id_abc_fkey | t            | parted_self_fk
  part3_self_fk  | parted_self_fk_id_abc_fkey | f       | t            | parted_self_fk_id_abc_fkey | t            | parted_self_fk
  parted_self_fk | parted_self_fk_id_abc_fkey | f       | t            |                            |              | parted_self_fk
+ part1_self_fk  | part1_self_fk_id_not_null  | n       | t            |                            |              | 
+ part2_self_fk  | parted_self_fk_id_not_null | n       | t            |                            |              | 
+ part32_self_fk | part3_self_fk_id_not_null  | n       | t            |                            |              | 
+ part33_self_fk | part33_self_fk_id_not_null | n       | t            |                            |              | 
+ part3_self_fk  | part3_self_fk_id_not_null  | n       | t            |                            |              | 
+ parted_self_fk | parted_self_fk_id_not_null | n       | t            |                            |              | 
  part1_self_fk  | part1_self_fk_pkey         | p       | t            | parted_self_fk_pkey        | t            | 
  part2_self_fk  | part2_self_fk_pkey         | p       | t            | parted_self_fk_pkey        | t            | 
  part32_self_fk | part32_self_fk_pkey        | p       | t            | part3_self_fk_pkey         | t            | 
  part33_self_fk | part33_self_fk_pkey        | p       | t            | part3_self_fk_pkey         | t            | 
  part3_self_fk  | part3_self_fk_pkey         | p       | t            | parted_self_fk_pkey        | t            | 
  parted_self_fk | parted_self_fk_pkey        | p       | t            |                            |              | 
-(12 rows)
+(18 rows)
 
 -- Leave this table around, for pg_upgrade/pg_dump tests
 -- Test creating a constraint at the parent that already exists in partitions.
 
  a      | integer |           | not null |                                     | plain   |              | 
  b      | integer |           |          | generated always as (a * 22) stored | plain   |              | 
  x      | integer |           |          |                                     | plain   |              | 
+Not-null constraints:
+    "gtestx_a_not_null" NOT NULL "a" (inherited)
 Inherits: gtest1
 
 CREATE TABLE gtestxx_1 (a int NOT NULL, b int);
 
  f3     | integer |           | not null | generated by default as identity | plain   |              | 
  f4     | bigint  |           | not null | generated always as identity     | plain   |              | 
  f5     | bigint  |           |          |                                  | plain   |              | 
+Not-null constraints:
+    "itest8_f2_not_null" NOT NULL "f2"
+    "itest8_f3_not_null" NOT NULL "f3"
+    "itest8_f4_not_null" NOT NULL "f4"
 
 \d itest8_f2_seq
                    Sequence "public.itest8_f2_seq"
 
 alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 30);
 select conname, contype, conrelid::regclass, conindid::regclass, conkey
   from pg_constraint where conrelid::regclass::text like 'idxpart%'
-  order by conname;
-    conname     | contype | conrelid  |    conindid    | conkey 
-----------------+---------+-----------+----------------+--------
- idxpart1_pkey  | p       | idxpart1  | idxpart1_pkey  | {1,2}
- idxpart21_pkey | p       | idxpart21 | idxpart21_pkey | {1,2}
- idxpart22_pkey | p       | idxpart22 | idxpart22_pkey | {1,2}
- idxpart2_pkey  | p       | idxpart2  | idxpart2_pkey  | {1,2}
- idxpart3_pkey  | p       | idxpart3  | idxpart3_pkey  | {2,1}
- idxpart_pkey   | p       | idxpart   | idxpart_pkey   | {1,2}
-(6 rows)
+  order by conrelid::regclass::text, conname;
+       conname       | contype | conrelid  |    conindid    | conkey 
+---------------------+---------+-----------+----------------+--------
+ idxpart_pkey        | p       | idxpart   | idxpart_pkey   | {1,2}
+ idxpart1_pkey       | p       | idxpart1  | idxpart1_pkey  | {1,2}
+ idxpart2_pkey       | p       | idxpart2  | idxpart2_pkey  | {1,2}
+ idxpart21_pkey      | p       | idxpart21 | idxpart21_pkey | {1,2}
+ idxpart22_pkey      | p       | idxpart22 | idxpart22_pkey | {1,2}
+ idxpart3_a_not_null | n       | idxpart3  | -              | {2}
+ idxpart3_b_not_null | n       | idxpart3  | -              | {1}
+ idxpart3_pkey       | p       | idxpart3  | idxpart3_pkey  | {2,1}
+(8 rows)
 
 drop table idxpart;
 -- Verify that multi-layer partitioning honors the requirement that all
 create table idxpart0 (like idxpart);
 alter table idxpart0 add unique (a);
 alter table idxpart attach partition idxpart0 default;
-alter table only idxpart add primary key (a);  -- fail, no NOT NULL constraint
-ERROR:  constraint must be added to child tables too
-DETAIL:  Column "a" of relation "idxpart0" is not already NOT NULL.
-HINT:  Do not specify the ONLY keyword.
+alter table only idxpart add primary key (a);  -- works, but idxpart0.a is nullable
+\d idxpart0
+              Table "public.idxpart0"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Partition of: idxpart DEFAULT
+Indexes:
+    "idxpart0_a_key" UNIQUE CONSTRAINT, btree (a)
+
+alter index idxpart_pkey attach partition idxpart0_a_key; -- fails, lacks NOT NULL
+ERROR:  invalid primary key definition
+DETAIL:  Column "a" of relation "idxpart0" is not marked NOT NULL.
 alter table idxpart0 alter column a set not null;
-alter table only idxpart add primary key (a);  -- now it works
+alter index idxpart_pkey attach partition idxpart0_a_key;
 alter table idxpart0 alter column a drop not null;  -- fail, pkey needs it
 ERROR:  column "a" is marked NOT NULL in parent table
 drop table idxpart;
 
 drop table cnullparent cascade;
 NOTICE:  drop cascades to table cnullchild
 --
+-- Test inheritance of NOT NULL constraints
+--
+create table pp1 (f1 int);
+create table cc1 (f2 text, f3 int) inherits (pp1);
+\d cc1
+                Table "public.cc1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ f1     | integer |           |          | 
+ f2     | text    |           |          | 
+ f3     | integer |           |          | 
+Inherits: pp1
+
+create table cc2(f4 float) inherits(pp1,cc1);
+NOTICE:  merging multiple inherited definitions of column "f1"
+\d cc2
+                     Table "public.cc2"
+ Column |       Type       | Collation | Nullable | Default 
+--------+------------------+-----------+----------+---------
+ f1     | integer          |           |          | 
+ f2     | text             |           |          | 
+ f3     | integer          |           |          | 
+ f4     | double precision |           |          | 
+Inherits: pp1,
+          cc1
+
+-- named NOT NULL constraint
+alter table cc1 add column a2 int constraint nn not null;
+\d+ cc1
+                                    Table "public.cc1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ f1     | integer |           |          |         | plain    |              | 
+ f2     | text    |           |          |         | extended |              | 
+ f3     | integer |           |          |         | plain    |              | 
+ a2     | integer |           | not null |         | plain    |              | 
+Not-null constraints:
+    "nn" NOT NULL "a2"
+Inherits: pp1
+Child tables: cc2
+
+\d+ cc2
+                                         Table "public.cc2"
+ Column |       Type       | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+------------------+-----------+----------+---------+----------+--------------+-------------
+ f1     | integer          |           |          |         | plain    |              | 
+ f2     | text             |           |          |         | extended |              | 
+ f3     | integer          |           |          |         | plain    |              | 
+ f4     | double precision |           |          |         | plain    |              | 
+ a2     | integer          |           | not null |         | plain    |              | 
+Not-null constraints:
+    "nn" NOT NULL "a2" (inherited)
+Inherits: pp1,
+          cc1
+
+alter table pp1 alter column f1 set not null;
+\d+ pp1
+                                    Table "public.pp1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ f1     | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "pp1_f1_not_null" NOT NULL "f1"
+Child tables: cc1,
+              cc2
+
+\d+ cc1
+                                    Table "public.cc1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ f1     | integer |           | not null |         | plain    |              | 
+ f2     | text    |           |          |         | extended |              | 
+ f3     | integer |           |          |         | plain    |              | 
+ a2     | integer |           | not null |         | plain    |              | 
+Not-null constraints:
+    "pp1_f1_not_null" NOT NULL "f1" (inherited)
+    "nn" NOT NULL "a2"
+Inherits: pp1
+Child tables: cc2
+
+\d+ cc2
+                                         Table "public.cc2"
+ Column |       Type       | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+------------------+-----------+----------+---------+----------+--------------+-------------
+ f1     | integer          |           | not null |         | plain    |              | 
+ f2     | text             |           |          |         | extended |              | 
+ f3     | integer          |           |          |         | plain    |              | 
+ f4     | double precision |           |          |         | plain    |              | 
+ a2     | integer          |           | not null |         | plain    |              | 
+Not-null constraints:
+    "pp1_f1_not_null" NOT NULL "f1" (inherited)
+    "nn" NOT NULL "a2" (inherited)
+Inherits: pp1,
+          cc1
+
+-- 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"
+-- remove constraint cc1, should succeed
+alter table cc1 alter column a2 drop not null;
+\d+ cc1
+                                    Table "public.cc1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+--------------+-------------
+ f1     | integer |           | not null |         | plain    |              | 
+ f2     | text    |           |          |         | extended |              | 
+ f3     | integer |           |          |         | plain    |              | 
+ a2     | integer |           |          |         | plain    |              | 
+Not-null constraints:
+    "pp1_f1_not_null" NOT NULL "f1" (inherited)
+Inherits: pp1
+Child tables: cc2
+
+-- same for cc2
+alter table cc2 alter column f1 drop not null;
+ERROR:  cannot drop inherited constraint "pp1_f1_not_null" of relation "cc2"
+\d+ cc2
+                                         Table "public.cc2"
+ Column |       Type       | Collation | Nullable | Default | Storage  | Stats target | Description 
+--------+------------------+-----------+----------+---------+----------+--------------+-------------
+ f1     | integer          |           | not null |         | plain    |              | 
+ f2     | text             |           |          |         | extended |              | 
+ f3     | integer          |           |          |         | plain    |              | 
+ f4     | double precision |           |          |         | plain    |              | 
+ a2     | integer          |           |          |         | plain    |              | 
+Not-null constraints:
+    "pp1_f1_not_null" NOT NULL "f1" (inherited)
+Inherits: pp1,
+          cc1
+
+-- remove from cc1, should fail again
+alter table cc1 alter column f1 drop not null;
+ERROR:  cannot drop inherited constraint "pp1_f1_not_null" of relation "cc1"
+-- remove from pp1, should succeed
+alter table pp1 alter column f1 drop not null;
+\d+ pp1
+                                    Table "public.pp1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ f1     | integer |           |          |         | plain   |              | 
+Child tables: cc1,
+              cc2
+
+alter table pp1 add primary key (f1);
+-- Leave these tables around, for pg_upgrade testing
+-- Test the same constraint name for different columns in different parents
+create table inh_parent1(a int constraint nn not null);
+create table inh_parent2(b int constraint nn not null);
+create table inh_child () inherits (inh_parent1, inh_parent2);
+\d+ inh_child
+                                 Table "public.inh_child"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "nn" NOT NULL "a" (inherited)
+    "inh_child_b_not_null" NOT NULL "b" (inherited)
+Inherits: inh_parent1,
+          inh_parent2
+
+drop table inh_parent1, inh_parent2, inh_child;
+-- Test multiple parents with overlapping primary keys
+create table inh_parent1(a int, b int, c int, primary key (a, b));
+create table inh_parent2(d int, e int, b int, primary key (d, b));
+create table inh_child() inherits (inh_parent1, inh_parent2);
+NOTICE:  merging multiple inherited definitions of column "b"
+select conrelid::regclass, conname, contype, conkey,
+ coninhcount, conislocal, connoinherit
+ from pg_constraint where contype in ('n','p') and
+ conrelid::regclass::text in ('inh_child', 'inh_parent1', 'inh_parent2')
+ order by 1, 2;
+  conrelid   |       conname        | contype | conkey | coninhcount | conislocal | connoinherit 
+-------------+----------------------+---------+--------+-------------+------------+--------------
+ inh_parent1 | inh_parent1_pkey     | p       | {1,2}  |           0 | t          | t
+ inh_parent2 | inh_parent2_pkey     | p       | {1,3}  |           0 | t          | t
+ inh_child   | inh_child_a_not_null | n       | {1}    |           1 | f          | f
+ inh_child   | inh_child_b_not_null | n       | {2}    |           2 | f          | f
+ inh_child   | inh_child_d_not_null | n       | {4}    |           1 | f          | f
+(5 rows)
+
+\d+ inh_child
+                                 Table "public.inh_child"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           | not null |         | plain   |              | 
+ c      | integer |           |          |         | plain   |              | 
+ d      | integer |           | not null |         | plain   |              | 
+ e      | integer |           |          |         | plain   |              | 
+Not-null constraints:
+    "inh_child_a_not_null" NOT NULL "a" (inherited)
+    "inh_child_b_not_null" NOT NULL "b" (inherited)
+    "inh_child_d_not_null" NOT NULL "d" (inherited)
+Inherits: inh_parent1,
+          inh_parent2
+
+drop table inh_parent1, inh_parent2, inh_child;
+-- NOT NULL NO INHERIT
+create table inh_nn_parent(a int);
+create table inh_nn_child() inherits (inh_nn_parent);
+alter table inh_nn_parent add not null a no inherit;
+create table inh_nn_child2() inherits (inh_nn_parent);
+select conrelid::regclass, conname, contype, conkey,
+ (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]),
+ coninhcount, conislocal, connoinherit
+ from pg_constraint where contype = 'n' and
+ conrelid::regclass::text like 'inh\_nn\_%'
+ order by 2, 1;
+   conrelid    |         conname          | contype | conkey | attname | coninhcount | conislocal | connoinherit 
+---------------+--------------------------+---------+--------+---------+-------------+------------+--------------
+ inh_nn_parent | inh_nn_parent_a_not_null | n       | {1}    | a       |           0 | t          | t
+(1 row)
+
+\d+ inh_nn*
+                               Table "public.inh_nn_child"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Inherits: inh_nn_parent
+
+                               Table "public.inh_nn_child2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Inherits: inh_nn_parent
+
+                               Table "public.inh_nn_parent"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "inh_nn_parent_a_not_null" NOT NULL "a" NO INHERIT
+Child tables: inh_nn_child,
+              inh_nn_child2
+
+drop table inh_nn_parent, inh_nn_child, inh_nn_child2;
+--
+-- test inherit/deinherit
+--
+create table inh_parent(f1 int);
+create table inh_child1(f1 int not null);
+create table inh_child2(f1 int);
+-- inh_child1 should have not null constraint
+alter table inh_child1 inherit inh_parent;
+-- should fail, missing NOT NULL constraint
+alter table inh_child2 inherit inh_child1;
+ERROR:  column "f1" in child table must be marked NOT NULL
+alter table inh_child2 alter column f1 set not null;
+alter table inh_child2 inherit inh_child1;
+-- add NOT NULL constraint recursively
+alter table inh_parent alter column f1 set not null;
+\d+ inh_parent
+                                Table "public.inh_parent"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ f1     | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "inh_parent_f1_not_null" NOT NULL "f1"
+Child tables: inh_child1
+
+\d+ inh_child1
+                                Table "public.inh_child1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ f1     | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "inh_child1_f1_not_null" NOT NULL "f1" (local, inherited)
+Inherits: inh_parent
+Child tables: inh_child2
+
+\d+ inh_child2
+                                Table "public.inh_child2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ f1     | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited)
+Inherits: inh_child1
+
+select conrelid::regclass, conname, contype, coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass)
+ order by 2, 1;
+  conrelid  |        conname         | contype | coninhcount | conislocal 
+------------+------------------------+---------+-------------+------------
+ inh_child1 | inh_child1_f1_not_null | n       |           1 | t
+ inh_child2 | inh_child2_f1_not_null | n       |           1 | t
+ inh_parent | inh_parent_f1_not_null | n       |           0 | t
+(3 rows)
+
+--
+-- test deinherit procedure
+--
+-- deinherit inh_child1
+create table inh_grandchld () inherits (inh_child1);
+alter table inh_child1 no inherit inh_parent;
+\d+ inh_parent
+                                Table "public.inh_parent"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ f1     | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "inh_parent_f1_not_null" NOT NULL "f1"
+
+\d+ inh_child1
+                                Table "public.inh_child1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ f1     | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "inh_child1_f1_not_null" NOT NULL "f1"
+Child tables: inh_child2,
+              inh_grandchld
+
+\d+ inh_child2
+                                Table "public.inh_child2"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ f1     | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "inh_child2_f1_not_null" NOT NULL "f1" (local, inherited)
+Inherits: inh_child1
+
+select conrelid::regclass, conname, contype, coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid::regclass::text in ('inh_parent', 'inh_child1', 'inh_child2', 'inh_grandchld')
+ order by 2, 1;
+   conrelid    |        conname         | contype | coninhcount | conislocal 
+---------------+------------------------+---------+-------------+------------
+ inh_child1    | inh_child1_f1_not_null | n       |           0 | t
+ inh_grandchld | inh_child1_f1_not_null | n       |           1 | f
+ inh_child2    | inh_child2_f1_not_null | n       |           1 | t
+ inh_parent    | inh_parent_f1_not_null | n       |           0 | t
+(4 rows)
+
+drop table inh_parent, inh_child1, inh_child2, inh_grandchld;
+-- a PK in parent must have a not-null in child that it can mark inherited
+create table inh_parent (a int primary key);
+create table inh_child (a int primary key);
+alter table inh_child inherit inh_parent;      -- nope
+ERROR:  column "a" in child table must be marked NOT NULL
+alter table inh_child alter a set not null;
+alter table inh_child inherit inh_parent;      -- now it works
+drop table inh_parent, inh_child;
+--
+-- test multi inheritance tree
+--
+create table inh_parent(f1 int not null);
+create table inh_child1() inherits(inh_parent);
+create table inh_child2() inherits(inh_parent);
+create table inh_grandchld() inherits(inh_child1, inh_child2);
+NOTICE:  merging multiple inherited definitions of column "f1"
+-- show constraint info
+select conrelid::regclass, conname, contype, coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass, 'inh_grandchld'::regclass)
+ order by 2, conrelid::regclass::text;
+   conrelid    |        conname         | contype | coninhcount | conislocal 
+---------------+------------------------+---------+-------------+------------
+ inh_child1    | inh_parent_f1_not_null | n       |           1 | f
+ inh_child2    | inh_parent_f1_not_null | n       |           1 | f
+ inh_grandchld | inh_parent_f1_not_null | n       |           2 | f
+ inh_parent    | inh_parent_f1_not_null | n       |           0 | t
+(4 rows)
+
+drop table inh_parent cascade;
+NOTICE:  drop cascades to 3 other objects
+DETAIL:  drop cascades to table inh_child1
+drop cascades to table inh_child2
+drop cascades to table inh_grandchld
+-- test child table with inherited columns and
+-- with explicitly specified not null constraints
+create table inh_parent_1(f1 int);
+create table inh_parent_2(f2 text);
+create table inh_child(f1 int not null, f2 text not null) inherits(inh_parent_1, inh_parent_2);
+NOTICE:  merging column "f1" with inherited definition
+NOTICE:  merging column "f2" with inherited definition
+-- show constraint info
+select conrelid::regclass, conname, contype, coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid in ('inh_parent_1'::regclass, 'inh_parent_2'::regclass, 'inh_child'::regclass)
+ order by 2, conrelid::regclass::text;
+ conrelid  |        conname        | contype | coninhcount | conislocal 
+-----------+-----------------------+---------+-------------+------------
+ inh_child | inh_child_f1_not_null | n       |           0 | t
+ inh_child | inh_child_f2_not_null | n       |           0 | t
+(2 rows)
+
+-- also drops inh_child table
+drop table inh_parent_1 cascade;
+NOTICE:  drop cascades to table inh_child
+drop table inh_parent_2;
+-- test multi layer inheritance tree
+create table inh_p1(f1 int not null);
+create table inh_p2(f1 int not null);
+create table inh_p3(f2 int);
+create table inh_p4(f1 int not null, f3 text not null);
+create table inh_multiparent() inherits(inh_p1, inh_p2, inh_p3, inh_p4);
+NOTICE:  merging multiple inherited definitions of column "f1"
+NOTICE:  merging multiple inherited definitions of column "f1"
+-- constraint on f1 should have three parents
+select conrelid::regclass, contype, conname,
+  (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]),
+  coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid::regclass in ('inh_p1', 'inh_p2', 'inh_p3', 'inh_p4',
+   'inh_multiparent')
+ order by conrelid::regclass::text, conname;
+    conrelid     | contype |      conname       | attname | coninhcount | conislocal 
+-----------------+---------+--------------------+---------+-------------+------------
+ inh_multiparent | n       | inh_p1_f1_not_null | f1      |           3 | f
+ inh_multiparent | n       | inh_p4_f3_not_null | f3      |           1 | f
+ inh_p1          | n       | inh_p1_f1_not_null | f1      |           0 | t
+ inh_p2          | n       | inh_p2_f1_not_null | f1      |           0 | t
+ inh_p4          | n       | inh_p4_f1_not_null | f1      |           0 | t
+ inh_p4          | n       | inh_p4_f3_not_null | f3      |           0 | t
+(6 rows)
+
+create table inh_multiparent2 (a int not null, f1 int) inherits(inh_p3, inh_multiparent);
+NOTICE:  merging multiple inherited definitions of column "f2"
+NOTICE:  merging column "f1" with inherited definition
+select conrelid::regclass, contype, conname,
+  (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]),
+  coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid::regclass in ('inh_p3', 'inh_multiparent', 'inh_multiparent2')
+ order by conrelid::regclass::text, conname;
+     conrelid     | contype |           conname           | attname | coninhcount | conislocal 
+------------------+---------+-----------------------------+---------+-------------+------------
+ inh_multiparent  | n       | inh_p1_f1_not_null          | f1      |           3 | f
+ inh_multiparent  | n       | inh_p4_f3_not_null          | f3      |           1 | f
+ inh_multiparent2 | n       | inh_multiparent2_a_not_null | a       |           0 | t
+ inh_multiparent2 | n       | inh_p1_f1_not_null          | f1      |           1 | f
+ inh_multiparent2 | n       | inh_p4_f3_not_null          | f3      |           1 | f
+(5 rows)
+
+drop table inh_p1, inh_p2, inh_p3, inh_p4 cascade;
+NOTICE:  drop cascades to 2 other objects
+DETAIL:  drop cascades to table inh_multiparent
+drop cascades to table inh_multiparent2
+--
 -- Check use of temporary tables with inheritance trees
 --
 create table inh_perm_parent (a1 int);
 
     "testpub_tbl2_pkey" PRIMARY KEY, btree (id)
 Publications:
     "testpub_foralltables"
+Not-null constraints:
+    "testpub_tbl2_id_not_null" NOT NULL "id"
 
 \dRp+ testpub_foralltables
                               Publication testpub_foralltables
     "testpib_ins_trunct"
     "testpub_default"
     "testpub_fortbl"
+Not-null constraints:
+    "testpub_tbl1_id_not_null" NOT NULL "id"
 
 \dRp+ testpub_default
                                 Publication testpub_default
 Publications:
     "testpib_ins_trunct"
     "testpub_fortbl"
+Not-null constraints:
+    "testpub_tbl1_id_not_null" NOT NULL "id"
 
 -- verify relation cache invalidation when a primary key is added using
 -- an existing index
 
     "test_replica_identity_partial" UNIQUE, btree (keya, keyb) WHERE keyb <> '3'::text
     "test_replica_identity_unique_defer" UNIQUE CONSTRAINT, btree (keya, keyb) DEFERRABLE
     "test_replica_identity_unique_nondefer" UNIQUE CONSTRAINT, btree (keya, keyb)
+Not-null constraints:
+    "test_replica_identity_id_not_null" NOT NULL "id"
+    "test_replica_identity_keya_not_null" NOT NULL "keya"
+    "test_replica_identity_keyb_not_null" NOT NULL "keyb"
 Replica Identity: FULL
 
 ALTER TABLE test_replica_identity REPLICA IDENTITY NOTHING;
 -- used as replica identity.
 ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL;
 ERROR:  column "id" is in index used as replica identity
+-- but it's OK when the identity is FULL
+ALTER TABLE test_replica_identity3 REPLICA IDENTITY FULL;
+ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL;
 --
 -- Test that replica identity can be set on an index that's not yet valid.
 -- (This matches the way pg_dump will try to dump a partitioned table.)
 Partition key: LIST (id)
 Indexes:
     "test_replica_identity4_pkey" PRIMARY KEY, btree (id) INVALID REPLICA IDENTITY
+Not-null constraints:
+    "test_replica_identity4_id_not_null" NOT NULL "id"
 Partitions: test_replica_identity4_1 FOR VALUES IN (1)
 
 ALTER INDEX test_replica_identity4_pkey
 Partition key: LIST (id)
 Indexes:
     "test_replica_identity4_pkey" PRIMARY KEY, btree (id) REPLICA IDENTITY
+Not-null constraints:
+    "test_replica_identity4_id_not_null" NOT NULL "id"
 Partitions: test_replica_identity4_1 FOR VALUES IN (1)
 
+-- Dropping the primary key is not allowed if that would leave the replica
+-- identity as nullable
+CREATE TABLE test_replica_identity5 (a int not null, b int, c int,
+   PRIMARY KEY (b, c));
+CREATE UNIQUE INDEX test_replica_identity5_a_b_key ON test_replica_identity5 (a, b);
+ALTER TABLE test_replica_identity5 REPLICA IDENTITY USING INDEX test_replica_identity5_a_b_key;
+ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey;
+ERROR:  column "b" is in index used as replica identity
+ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL;
+ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey;
+ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL;
+ERROR:  column "b" is in index used as replica identity
 DROP TABLE test_replica_identity;
 DROP TABLE test_replica_identity2;
 DROP TABLE test_replica_identity3;
 DROP TABLE test_replica_identity4;
+DROP TABLE test_replica_identity5;
 DROP TABLE test_replica_identity_othertable;
 
     POLICY "pp1r" AS RESTRICTIVE
       TO regress_rls_dave
       USING ((cid < 55))
+Not-null constraints:
+    "part_document_dlevel_not_null" NOT NULL "dlevel"
 Partitions: part_document_fiction FOR VALUES FROM (11) TO (12),
             part_document_nonfiction FOR VALUES FROM (99) TO (100),
             part_document_satire FOR VALUES FROM (55) TO (56)
 
 -- test checking for null values and primary key
 create table atacc1 (test int not null);
 alter table atacc1 add constraint "atacc1_pkey" primary key (test);
+\d atacc1
 alter table atacc1 alter column test drop not null;
+\d atacc1
 alter table atacc1 drop constraint "atacc1_pkey";
-alter table atacc1 alter column test drop not null;
+\d atacc1
 insert into atacc1 values (null);
 alter table atacc1 alter test set not null;
 delete from atacc1;
 insert into child (a, b) values (NULL, 'foo');
 alter table only parent alter a set not null;
 alter table child alter a set not null;
-delete from parent;
-alter table only parent alter a set not null;
-insert into parent values (NULL);
-alter table child alter a set not null;
-insert into child (a, b) values (NULL, 'foo');
-delete from child;
-alter table child alter a set not null;
-insert into child (a, b) values (NULL, 'foo');
 drop table child;
 drop table parent;
 
 \d ataddindex
 DROP TABLE ataddindex;
 
+CREATE TABLE atnotnull1 ();
+ALTER TABLE atnotnull1
+  ADD COLUMN a INT,
+  ALTER a SET NOT NULL;
+ALTER TABLE atnotnull1
+  ADD COLUMN b INT,
+  ADD NOT NULL b;
+ALTER TABLE atnotnull1
+  ADD COLUMN c INT,
+  ADD PRIMARY KEY (c);
+\d+ atnotnull1
+
 -- cannot drop column that is part of the partition key
 CREATE TABLE partitioned (
    a int,
 
 INSERT INTO ATACC1 (TEST2) VALUES (3);
 DROP TABLE ATACC1 CASCADE;
 
+-- NOT NULL NO INHERIT
+CREATE TABLE ATACC1 (a int, not null a no inherit);
+CREATE TABLE ATACC2 () INHERITS (ATACC1);
+\d+ ATACC2
+DROP TABLE ATACC1, ATACC2;
+CREATE TABLE ATACC1 (a int);
+ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT;
+CREATE TABLE ATACC2 () INHERITS (ATACC1);
+\d+ ATACC2
+DROP TABLE ATACC1, ATACC2;
+CREATE TABLE ATACC1 (a int);
+CREATE TABLE ATACC2 () INHERITS (ATACC1);
+ALTER TABLE ATACC1 ADD NOT NULL a NO INHERIT;
+\d+ ATACC2
+DROP TABLE ATACC1, ATACC2;
+
 --
 -- Check constraints on INSERT INTO
 --
 
 DROP TABLE deferred_excl;
 
+-- verify constraints created for NOT NULL clauses
+CREATE TABLE notnull_tbl1 (a INTEGER NOT NULL NOT NULL);
+\d+ notnull_tbl1
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+-- no-op
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+\d+ notnull_tbl1
+-- duplicate name
+ALTER TABLE notnull_tbl1 ADD COLUMN b INT CONSTRAINT notnull_tbl1_a_not_null NOT NULL;
+-- DROP NOT NULL gets rid of both the attnotnull flag and the constraint itself
+ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL;
+\d notnull_tbl1
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+-- SET NOT NULL puts both back
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+\d notnull_tbl1
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+-- Doing it twice doesn't create a redundant constraint
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+-- Using the "table constraint" syntax also works
+ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
+\d notnull_tbl1
+select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass;
+DROP TABLE notnull_tbl1;
+
+-- nope
+CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL);
+
+CREATE TABLE notnull_tbl2 (a INTEGER PRIMARY KEY);
+ALTER TABLE notnull_tbl2 ALTER a DROP NOT NULL;
+
+CREATE TABLE notnull_tbl3 (a INTEGER NOT NULL, CHECK (a IS NOT NULL));
+ALTER TABLE notnull_tbl3 ALTER A DROP NOT NULL;
+ALTER TABLE notnull_tbl3 ADD b int, ADD CONSTRAINT pk PRIMARY KEY (a, b);
+\d notnull_tbl3
+ALTER TABLE notnull_tbl3 DROP CONSTRAINT pk;
+\d notnull_tbl3
+
+-- Primary keys in parent table cause NOT NULL constraint to spawn on their
+-- children.  Verify that they work correctly.
+CREATE TABLE cnn_parent (a int, b int);
+CREATE TABLE cnn_child () INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild (NOT NULL b) INHERITS (cnn_child);
+CREATE TABLE cnn_child2 (NOT NULL a NO INHERIT) INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild2 () INHERITS (cnn_grandchild, cnn_child2);
+
+ALTER TABLE cnn_parent ADD PRIMARY KEY (b);
+\d+ cnn_grandchild
+\d+ cnn_grandchild2
+ALTER TABLE cnn_parent DROP CONSTRAINT cnn_parent_pkey;
+\set VERBOSITY terse
+DROP TABLE cnn_parent CASCADE;
+\set VERBOSITY default
+
+-- As above, but create the primary key ahead of time
+CREATE TABLE cnn_parent (a int, b int PRIMARY KEY);
+CREATE TABLE cnn_child () INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild (NOT NULL b) INHERITS (cnn_child);
+CREATE TABLE cnn_child2 (NOT NULL a NO INHERIT) INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild2 () INHERITS (cnn_grandchild, cnn_child2);
+
+ALTER TABLE cnn_parent ADD PRIMARY KEY (b);
+\d+ cnn_grandchild
+\d+ cnn_grandchild2
+ALTER TABLE cnn_parent DROP CONSTRAINT cnn_parent_pkey;
+\set VERBOSITY terse
+DROP TABLE cnn_parent CASCADE;
+\set VERBOSITY default
+
+-- As above, but create the primary key using a UNIQUE index
+CREATE TABLE cnn_parent (a int, b int);
+CREATE TABLE cnn_child () INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild (NOT NULL b) INHERITS (cnn_child);
+CREATE TABLE cnn_child2 (NOT NULL a NO INHERIT) INHERITS (cnn_parent);
+CREATE TABLE cnn_grandchild2 () INHERITS (cnn_grandchild, cnn_child2);
+
+CREATE UNIQUE INDEX b_uq ON cnn_parent (b);
+ALTER TABLE cnn_parent ADD PRIMARY KEY USING INDEX b_uq;
+\d+ cnn_grandchild
+\d+ cnn_grandchild2
+ALTER TABLE cnn_parent DROP CONSTRAINT cnn_parent_pkey;
+-- keeps these tables around, for pg_upgrade testing
+
+
 -- Comments
 -- Setup a low-level role to enforce non-superuser checks.
 CREATE ROLE regress_constraint_comments;
 
    CONSTRAINT check_b CHECK (b >= 0)
 ) FOR VALUES IN ('b');
 -- conislocal should be false for any merged constraints, true otherwise
-SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY conislocal, coninhcount;
+SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname;
 
 -- Once check_b is added to the parent, it should be made non-local for part_b
 ALTER TABLE parted ADD CONSTRAINT check_b CHECK (b >= 0);
-SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass;
+SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname;
 
 -- Neither check_a nor check_b are droppable from part_b
 ALTER TABLE part_b DROP CONSTRAINT check_a;
 -- traditional inheritance where they will be left behind, because they would
 -- be local constraints.
 ALTER TABLE parted DROP CONSTRAINT check_a, DROP CONSTRAINT check_b;
-SELECT conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass;
+SELECT conname, conislocal, coninhcount FROM pg_constraint WHERE conrelid = 'part_b'::regclass ORDER BY coninhcount DESC, conname;
 
 -- specify PARTITION BY for a partition
 CREATE TABLE fail_part_col_not_found PARTITION OF parted FOR VALUES IN ('c') PARTITION BY RANGE (c);
 
 alter table idxpart attach partition idxpart3 for values from (20, 20) to (30, 30);
 select conname, contype, conrelid::regclass, conindid::regclass, conkey
   from pg_constraint where conrelid::regclass::text like 'idxpart%'
-  order by conname;
+  order by conrelid::regclass::text, conname;
 drop table idxpart;
 
 -- Verify that multi-layer partitioning honors the requirement that all
 create table idxpart0 (like idxpart);
 alter table idxpart0 add unique (a);
 alter table idxpart attach partition idxpart0 default;
-alter table only idxpart add primary key (a);  -- fail, no NOT NULL constraint
+alter table only idxpart add primary key (a);  -- works, but idxpart0.a is nullable
+\d idxpart0
+alter index idxpart_pkey attach partition idxpart0_a_key; -- fails, lacks NOT NULL
 alter table idxpart0 alter column a set not null;
-alter table only idxpart add primary key (a);  -- now it works
+alter index idxpart_pkey attach partition idxpart0_a_key;
 alter table idxpart0 alter column a drop not null;  -- fail, pkey needs it
 drop table idxpart;
 
 
 select * from cnullparent where f1 = 2;
 drop table cnullparent cascade;
 
+--
+-- Test inheritance of NOT NULL constraints
+--
+create table pp1 (f1 int);
+create table cc1 (f2 text, f3 int) inherits (pp1);
+\d cc1
+create table cc2(f4 float) inherits(pp1,cc1);
+\d cc2
+
+-- named NOT NULL constraint
+alter table cc1 add column a2 int constraint nn not null;
+\d+ cc1
+\d+ cc2
+alter table pp1 alter column f1 set not null;
+\d+ pp1
+\d+ cc1
+\d+ cc2
+
+-- remove constraint from cc2: no dice, it's inherited
+alter table cc2 alter column a2 drop not null;
+
+-- remove constraint cc1, should succeed
+alter table cc1 alter column a2 drop not null;
+\d+ cc1
+
+-- same for cc2
+alter table cc2 alter column f1 drop not null;
+\d+ cc2
+
+-- remove from cc1, should fail again
+alter table cc1 alter column f1 drop not null;
+
+-- remove from pp1, should succeed
+alter table pp1 alter column f1 drop not null;
+\d+ pp1
+
+alter table pp1 add primary key (f1);
+-- Leave these tables around, for pg_upgrade testing
+
+-- Test the same constraint name for different columns in different parents
+create table inh_parent1(a int constraint nn not null);
+create table inh_parent2(b int constraint nn not null);
+create table inh_child () inherits (inh_parent1, inh_parent2);
+\d+ inh_child
+drop table inh_parent1, inh_parent2, inh_child;
+
+-- Test multiple parents with overlapping primary keys
+create table inh_parent1(a int, b int, c int, primary key (a, b));
+create table inh_parent2(d int, e int, b int, primary key (d, b));
+create table inh_child() inherits (inh_parent1, inh_parent2);
+select conrelid::regclass, conname, contype, conkey,
+ coninhcount, conislocal, connoinherit
+ from pg_constraint where contype in ('n','p') and
+ conrelid::regclass::text in ('inh_child', 'inh_parent1', 'inh_parent2')
+ order by 1, 2;
+\d+ inh_child
+drop table inh_parent1, inh_parent2, inh_child;
+
+-- NOT NULL NO INHERIT
+create table inh_nn_parent(a int);
+create table inh_nn_child() inherits (inh_nn_parent);
+alter table inh_nn_parent add not null a no inherit;
+create table inh_nn_child2() inherits (inh_nn_parent);
+select conrelid::regclass, conname, contype, conkey,
+ (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]),
+ coninhcount, conislocal, connoinherit
+ from pg_constraint where contype = 'n' and
+ conrelid::regclass::text like 'inh\_nn\_%'
+ order by 2, 1;
+\d+ inh_nn*
+drop table inh_nn_parent, inh_nn_child, inh_nn_child2;
+
+--
+-- test inherit/deinherit
+--
+create table inh_parent(f1 int);
+create table inh_child1(f1 int not null);
+create table inh_child2(f1 int);
+
+-- inh_child1 should have not null constraint
+alter table inh_child1 inherit inh_parent;
+
+-- should fail, missing NOT NULL constraint
+alter table inh_child2 inherit inh_child1;
+
+alter table inh_child2 alter column f1 set not null;
+alter table inh_child2 inherit inh_child1;
+
+-- add NOT NULL constraint recursively
+alter table inh_parent alter column f1 set not null;
+
+\d+ inh_parent
+\d+ inh_child1
+\d+ inh_child2
+
+select conrelid::regclass, conname, contype, coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass)
+ order by 2, 1;
+
+--
+-- test deinherit procedure
+--
+
+-- deinherit inh_child1
+create table inh_grandchld () inherits (inh_child1);
+alter table inh_child1 no inherit inh_parent;
+\d+ inh_parent
+\d+ inh_child1
+\d+ inh_child2
+select conrelid::regclass, conname, contype, coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid::regclass::text in ('inh_parent', 'inh_child1', 'inh_child2', 'inh_grandchld')
+ order by 2, 1;
+drop table inh_parent, inh_child1, inh_child2, inh_grandchld;
+
+-- a PK in parent must have a not-null in child that it can mark inherited
+create table inh_parent (a int primary key);
+create table inh_child (a int primary key);
+alter table inh_child inherit inh_parent;      -- nope
+alter table inh_child alter a set not null;
+alter table inh_child inherit inh_parent;      -- now it works
+drop table inh_parent, inh_child;
+
+--
+-- test multi inheritance tree
+--
+create table inh_parent(f1 int not null);
+create table inh_child1() inherits(inh_parent);
+create table inh_child2() inherits(inh_parent);
+create table inh_grandchld() inherits(inh_child1, inh_child2);
+
+-- show constraint info
+select conrelid::regclass, conname, contype, coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid in ('inh_parent'::regclass, 'inh_child1'::regclass, 'inh_child2'::regclass, 'inh_grandchld'::regclass)
+ order by 2, conrelid::regclass::text;
+
+drop table inh_parent cascade;
+
+-- test child table with inherited columns and
+-- with explicitly specified not null constraints
+create table inh_parent_1(f1 int);
+create table inh_parent_2(f2 text);
+create table inh_child(f1 int not null, f2 text not null) inherits(inh_parent_1, inh_parent_2);
+
+-- show constraint info
+select conrelid::regclass, conname, contype, coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid in ('inh_parent_1'::regclass, 'inh_parent_2'::regclass, 'inh_child'::regclass)
+ order by 2, conrelid::regclass::text;
+
+-- also drops inh_child table
+drop table inh_parent_1 cascade;
+drop table inh_parent_2;
+
+-- test multi layer inheritance tree
+create table inh_p1(f1 int not null);
+create table inh_p2(f1 int not null);
+create table inh_p3(f2 int);
+create table inh_p4(f1 int not null, f3 text not null);
+
+create table inh_multiparent() inherits(inh_p1, inh_p2, inh_p3, inh_p4);
+
+-- constraint on f1 should have three parents
+select conrelid::regclass, contype, conname,
+  (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]),
+  coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid::regclass in ('inh_p1', 'inh_p2', 'inh_p3', 'inh_p4',
+   'inh_multiparent')
+ order by conrelid::regclass::text, conname;
+
+create table inh_multiparent2 (a int not null, f1 int) inherits(inh_p3, inh_multiparent);
+select conrelid::regclass, contype, conname,
+  (select attname from pg_attribute where attrelid = conrelid and attnum = conkey[1]),
+  coninhcount, conislocal
+ from pg_constraint where contype = 'n' and
+ conrelid::regclass in ('inh_p3', 'inh_multiparent', 'inh_multiparent2')
+ order by conrelid::regclass::text, conname;
+
+drop table inh_p1, inh_p2, inh_p3, inh_p4 cascade;
+
 --
 -- Check use of temporary tables with inheritance trees
 --
 
 -- ALTER TABLE DROP NOT NULL is not allowed for columns part of an index
 -- used as replica identity.
 ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL;
+-- but it's OK when the identity is FULL
+ALTER TABLE test_replica_identity3 REPLICA IDENTITY FULL;
+ALTER TABLE test_replica_identity3 ALTER COLUMN id DROP NOT NULL;
 
 --
 -- Test that replica identity can be set on an index that's not yet valid.
   ATTACH PARTITION test_replica_identity4_1_pkey;
 \d+ test_replica_identity4
 
+-- Dropping the primary key is not allowed if that would leave the replica
+-- identity as nullable
+CREATE TABLE test_replica_identity5 (a int not null, b int, c int,
+   PRIMARY KEY (b, c));
+CREATE UNIQUE INDEX test_replica_identity5_a_b_key ON test_replica_identity5 (a, b);
+ALTER TABLE test_replica_identity5 REPLICA IDENTITY USING INDEX test_replica_identity5_a_b_key;
+ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey;
+ALTER TABLE test_replica_identity5 ALTER b SET NOT NULL;
+ALTER TABLE test_replica_identity5 DROP CONSTRAINT test_replica_identity5_pkey;
+ALTER TABLE test_replica_identity5 ALTER b DROP NOT NULL;
+
 DROP TABLE test_replica_identity;
 DROP TABLE test_replica_identity2;
 DROP TABLE test_replica_identity3;
 DROP TABLE test_replica_identity4;
+DROP TABLE test_replica_identity5;
 DROP TABLE test_replica_identity_othertable;