Add pg_constraint rows for not-null constraints
authorÁlvaro Herrera <[email protected]>
Fri, 8 Nov 2024 12:28:48 +0000 (13:28 +0100)
committerÁlvaro Herrera <[email protected]>
Fri, 8 Nov 2024 12:28:48 +0000 (13:28 +0100)
We now create contype='n' pg_constraint rows for not-null constraints on
user tables.  Only one such constraint is allowed for a column.

We propagate these constraints to other tables during operations such as
adding inheritance relationships, creating and attaching partitions and
creating tables LIKE other tables.  These related constraints mostly
follow the well-known rules of conislocal and coninhcount that we have
for CHECK constraints, with some adaptations: for example, as opposed to
CHECK constraints, we don't match not-null ones by name when descending
a hierarchy to alter or remove it, instead matching by the name of the
column that they apply to.  This means we don't require the constraint
names to be identical across a hierarchy.

The inheritance status of these constraints can be controlled: now we
can be sure that if a parent table has one, then all children will have
it as well.  They can optionally be marked NO INHERIT, and then children
are free not to have one.  (There's currently no support for altering a
NO INHERIT constraint into inheriting down the hierarchy, but that's a
desirable future feature.)

This also opens the door for having these constraints be marked NOT
VALID, as well as allowing UNIQUE+NOT NULL to be used for functional
dependency determination, as envisioned by commit e49ae8d3bc58.  It's
likely possible to allow DEFERRABLE constraints as followup work, as
well.

psql shows these constraints in \d+, though we may want to reconsider if
this turns out to be too noisy.  Earlier versions of this patch hid
constraints that were on the same columns of the primary key, but I'm
not sure that that's very useful.  If clutter is a problem, we might be
better off inventing a new \d++ command and not showing the constraints
in \d+.

For now, we omit these constraints on system catalog columns, because
they're unlikely to achieve anything.

The main difference to the previous attempt at this (b0e96f311985) is
that we now require that such a constraint always exists when a primary
key is in the column; we didn't require this previously which had a
number of unpalatable consequences.  With this requirement, the code is
easier to reason about.  For example:

- We no longer have "throwaway constraints" during pg_dump.  We needed
  those for the case where a table had a PK without a not-null
  underneath, to prevent a slow scan of the data during restore of the
  PK creation, which was particularly problematic for pg_upgrade.

- We no longer have to cope with attnotnull being set spuriously in
  case a primary key is dropped indirectly (e.g., via DROP COLUMN).

Some bits of code in this patch were authored by Jian He.

Author: Álvaro Herrera <[email protected]>
Author: Bernd Helmle <[email protected]>
Reviewed-by: 何建 (jian he) <[email protected]>
Reviewed-by: 王刚 (Tender Wang) <[email protected]>
Reviewed-by: Justin Pryzby <[email protected]>
Reviewed-by: Peter Eisentraut <[email protected]>
Reviewed-by: Dean Rasheed <[email protected]>
Discussion: https://postgr.es/m/202408310358[email protected]

57 files changed:
contrib/sepgsql/expected/alter.out
contrib/test_decoding/expected/ddl.out
doc/src/sgml/catalogs.sgml
doc/src/sgml/ddl.sgml
doc/src/sgml/ref/alter_foreign_table.sgml
doc/src/sgml/ref/alter_table.sgml
doc/src/sgml/ref/create_foreign_table.sgml
doc/src/sgml/ref/create_table.sgml
src/backend/catalog/heap.c
src/backend/catalog/information_schema.sql
src/backend/catalog/pg_constraint.c
src/backend/commands/tablecmds.c
src/backend/commands/typecmds.c
src/backend/nodes/makefuncs.c
src/backend/optimizer/util/plancat.c
src/backend/parser/gram.y
src/backend/parser/parse_utilcmd.c
src/backend/replication/logical/relation.c
src/backend/utils/adt/ruleutils.c
src/backend/utils/cache/relcache.c
src/bin/pg_dump/common.c
src/bin/pg_dump/pg_dump.c
src/bin/pg_dump/pg_dump.h
src/bin/pg_dump/t/002_pg_dump.pl
src/bin/psql/describe.c
src/bin/psql/t/010_tab_completion.pl
src/include/catalog/catversion.h
src/include/catalog/heap.h
src/include/catalog/pg_constraint.h
src/include/nodes/makefuncs.h
src/include/nodes/parsenodes.h
src/include/utils/relcache.h
src/test/modules/test_ddl_deparse/expected/alter_table.out
src/test/modules/test_ddl_deparse/expected/create_table.out
src/test/modules/test_ddl_deparse/test_ddl_deparse.c
src/test/regress/expected/alter_table.out
src/test/regress/expected/cluster.out
src/test/regress/expected/constraints.out
src/test/regress/expected/create_table.out
src/test/regress/expected/create_table_like.out
src/test/regress/expected/foreign_data.out
src/test/regress/expected/foreign_key.out
src/test/regress/expected/generated_stored.out
src/test/regress/expected/identity.out
src/test/regress/expected/index_including.out
src/test/regress/expected/indexing.out
src/test/regress/expected/inherit.out
src/test/regress/expected/publication.out
src/test/regress/expected/replica_identity.out
src/test/regress/expected/rowsecurity.out
src/test/regress/sql/alter_table.sql
src/test/regress/sql/constraints.sql
src/test/regress/sql/create_table_like.sql
src/test/regress/sql/index_including.sql
src/test/regress/sql/indexing.sql
src/test/regress/sql/inherit.sql
src/test/regress/sql/replica_identity.sql

index ae43537505296755097a24facfce6261380ba750..1462cfa3cbcc700f1d024133d6158d2a3dedf648 100644 (file)
@@ -164,7 +164,6 @@ LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_re
 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
@@ -249,8 +248,6 @@ LOG:  SELinux: allowed { setattr } scontext=unconfined_u:unconfined_r:sepgsql_re
 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
index 5713b8ab1c375c07a94e4835223f0b18bbdb9094..bcd1f74b2bc530d4f8ce30c4fee229f1cf125b02 100644 (file)
@@ -492,6 +492,9 @@ WITH (user_catalog_table = true)
  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)
@@ -506,6 +509,9 @@ ALTER TABLE replication_metadata RESET (user_catalog_table);
  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']);
@@ -519,6 +525,9 @@ ALTER TABLE replication_metadata SET (user_catalog_table = true);
  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)
@@ -538,6 +547,9 @@ ALTER TABLE replication_metadata SET (user_catalog_table = false);
  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)
index 964c819a02d000a935de3e5f39f85d23df1d5cd1..c180ed7abbcd8b31e64d634b1cbba5ebf0a0db05 100644 (file)
        <structfield>attnotnull</structfield> <type>bool</type>
       </para>
       <para>
-       This represents a not-null constraint.
+       This column has a not-null constraint.
       </para></entry>
      </row>
 
@@ -2502,14 +2502,10 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </indexterm>
 
   <para>
-   The catalog <structname>pg_constraint</structname> stores check, primary
-   key, unique, foreign key, and exclusion constraints on tables, as well as
-   not-null constraints on domains.
+   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 on relations are represented in the
-   <link linkend="catalog-pg-attribute"><structname>pg_attribute</structname></link>
-   catalog, not here.
   </para>
 
   <para>
@@ -2571,7 +2567,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        <literal>c</literal> = check constraint,
        <literal>f</literal> = foreign key constraint,
-       <literal>n</literal> = not-null constraint (domains only),
+       <literal>n</literal> = not-null constraint,
        <literal>p</literal> = primary key constraint,
        <literal>u</literal> = unique constraint,
        <literal>t</literal> = constraint trigger,
index 898b6ddc8dfafd5c92a9e5ceb911d3766c9c4050..3c56610d2ac6c4f802e9db0ddaaa416e3a65d6a7 100644 (file)
@@ -772,17 +772,38 @@ CREATE TABLE products (
     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>
@@ -799,6 +820,10 @@ CREATE TABLE products (
     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
@@ -992,7 +1017,7 @@ CREATE TABLE example (
 
    <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
@@ -1652,11 +1677,16 @@ ALTER TABLE products ADD CHECK (name &lt;&gt; '');
 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>
@@ -1697,12 +1727,15 @@ ALTER TABLE products DROP CONSTRAINT some_name;
    </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>
 
@@ -4446,12 +4479,10 @@ ALTER INDEX measurement_city_id_logdate_key
        <para>
         Both <literal>CHECK</literal> and <literal>NOT NULL</literal>
         constraints of a partitioned table are always inherited by all its
-        partitions.  <literal>CHECK</literal> constraints that are marked
-        <literal>NO INHERIT</literal> are not allowed to be created on
-        partitioned tables.
-        You cannot drop a <literal>NOT NULL</literal> constraint on a
-        partition's column if the same constraint is present in the parent
-        table.
+        partitions; it is not allowed to create <literal>NO INHERIT</literal>
+        constraints of those types.
+        You cannot drop a constraint of those types if the same constraint
+        is present in the parent table.
        </para>
       </listitem>
 
index 3cb6f08fcf2a9b3f83783010ea0f51b24d1062e6..e2da3cc719f900a051f1ca0c41def57b879484cd 100644 (file)
@@ -173,7 +173,8 @@ ALTER FOREIGN TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceab
      <para>
       This form adds a new constraint to a foreign table, using the same
       syntax as <link linkend="sql-createforeigntable"><command>CREATE FOREIGN TABLE</command></link>.
-      Currently only <literal>CHECK</literal> constraints are supported.
+      Currently only <literal>CHECK</literal> and <literal>NOT NULL</literal>
+      constraints are supported.
      </para>
 
      <para>
@@ -182,7 +183,8 @@ ALTER FOREIGN TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceab
       declares that some new condition should be assumed to hold for all rows
       in the foreign table.  (See the discussion
       in <link linkend="sql-createforeigntable"><command>CREATE FOREIGN TABLE</command></link>.)
-      If the constraint is marked <literal>NOT VALID</literal>, then it isn't
+      If the constraint is marked <literal>NOT VALID</literal> (allowed only for
+      the <literal>CHECK</literal> case), then it isn't
       assumed to hold, but is only recorded for possible future use.
      </para>
     </listitem>
index 61a0fb3dec147acd577b8d1170b6e6f0d0872320..6098ebed4334bb3afde17771468d6a4cbe983181 100644 (file)
@@ -98,7 +98,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 <phrase>and <replaceable class="parameter">column_constraint</replaceable> is:</phrase>
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
-{ NOT NULL |
+{ NOT NULL [ NO INHERIT ] |
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   DEFAULT <replaceable>default_expr</replaceable> |
@@ -114,6 +114,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
 
 [ 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> ) ] |
@@ -849,19 +850,16 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       table.  Subsequently, queries against the parent will include records
       of the target table.  To be added as a child, the target table must
       already contain all the same columns as the parent (it could have
-      additional columns, too).  The columns must have matching data types,
-      and if they have <literal>NOT NULL</literal> constraints in the parent
-      then they must also have <literal>NOT NULL</literal> constraints in the
-      child.
+      additional columns, too).  The columns must have matching data types.
      </para>
 
      <para>
-      There must also be matching child-table constraints for all
-      <literal>CHECK</literal> constraints of the parent, except those
-      marked non-inheritable (that is, created with <literal>ALTER TABLE ... ADD CONSTRAINT ... NO INHERIT</literal>)
-      in the parent, which are ignored; all child-table constraints matched
-      must not be marked non-inheritable.
-      Currently
+      In addition, all <literal>CHECK</literal> and <literal>NOT NULL</literal>
+      constraints on the parent must also exist on the child, except those
+      marked non-inheritable (that is, created with
+      <literal>ALTER TABLE ... ADD CONSTRAINT ... NO INHERIT</literal>), which
+      are ignored.  All child-table constraints matched must not be marked
+      non-inheritable.  Currently
       <literal>UNIQUE</literal>, <literal>PRIMARY KEY</literal>, and
       <literal>FOREIGN KEY</literal> constraints are not considered, but
       this might change in the future.
@@ -1793,11 +1791,17 @@ ALTER TABLE measurement
   <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.
index dc4b90759901b391ff16f7220b91fbe869bbd139..fc81ba3c4985755fedef8049e6ec3347d21304e6 100644 (file)
@@ -43,7 +43,7 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name
 <phrase>where <replaceable class="parameter">column_constraint</replaceable> is:</phrase>
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
-{ NOT NULL |
+{ NOT NULL [ NO INHERIT ] |
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   DEFAULT <replaceable>default_expr</replaceable> |
@@ -52,6 +52,7 @@ CREATE FOREIGN TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name
 <phrase>and <replaceable class="parameter">table_constraint</replaceable> is:</phrase>
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
+  NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
 CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ]
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
@@ -203,11 +204,16 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    </varlistentry>
 
    <varlistentry>
-    <term><literal>NOT NULL</literal></term>
+    <term><literal>NOT NULL</literal> [ NO INHERIT ]</term>
     <listitem>
      <para>
       The column is not allowed to contain null values.
      </para>
+
+     <para>
+      A constraint marked with <literal>NO INHERIT</literal> will not propagate to
+      child tables.
+     </para>
     </listitem>
    </varlistentry>
 
index 83859bac76f77a2278fa110acdbedb0c8430efbe..dd83b07d65f0d3b46ca3ab174908b3de1c815cc8 100644 (file)
@@ -61,7 +61,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
 <phrase>where <replaceable class="parameter">column_constraint</replaceable> is:</phrase>
 
 [ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
-{ NOT NULL |
+{ NOT NULL [ NO INHERIT ]  |
   NULL |
   CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
   DEFAULT <replaceable>default_expr</replaceable> |
@@ -77,6 +77,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
 
 [ 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">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
   PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <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> ) ] |
@@ -818,11 +819,16 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
    </varlistentry>
 
    <varlistentry id="sql-createtable-parms-not-null">
-    <term><literal>NOT NULL</literal></term>
+    <term><literal>NOT NULL [ NO INHERIT ] </literal></term>
     <listitem>
      <para>
       The column is not allowed to contain null values.
      </para>
+
+     <para>
+      A constraint marked with <literal>NO INHERIT</literal> will not propagate to
+      child tables.
+     </para>
     </listitem>
    </varlistentry>
 
@@ -2398,13 +2404,6 @@ CREATE TABLE cities_partdef
     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 not-null constraints at all, so they are not
-    subject to the uniqueness restriction.  This might change in a future
-    release.
-   </para>
   </refsect2>
 
   <refsect2>
index c54a543c5365a428c7bea5358af22180aeeeede4..003af4bf21c5e70b118f3b1983de4333a2cbba67 100644 (file)
@@ -2173,6 +2173,56 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
        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;
+
+       Assert(attnum > InvalidAttrNumber);
+
+       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,
+                                                         false);
+       return constrOid;
+}
+
 /*
  * Store defaults and constraints (passed as a list of CookedConstraint).
  *
@@ -2217,6 +2267,14 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
                                                                  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);
@@ -2245,7 +2303,7 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
  *             cooked CHECK constraints
  *
  * All entries in newColDefaults will be processed.  Entries in newConstraints
- * will be processed only if they are CONSTR_CHECK type.
+ * will be processed only if they are CONSTR_CHECK or CONSTR_NOTNULL types.
  *
  * Returns a list of CookedConstraint nodes that shows the cooked form of
  * the default and constraint expressions added to the relation.
@@ -2274,6 +2332,7 @@ AddRelationNewConstraints(Relation rel,
        ParseNamespaceItem *nsitem;
        int                     numchecks;
        List       *checknames;
+       List       *nnnames;
        Node       *expr;
        CookedConstraint *cooked;
 
@@ -2358,6 +2417,7 @@ AddRelationNewConstraints(Relation rel,
         */
        numchecks = numoldchecks;
        checknames = NIL;
+       nnnames = NIL;
        foreach_node(Constraint, cdef, newConstraints)
        {
                Oid                     constrOid;
@@ -2412,7 +2472,7 @@ AddRelationNewConstraints(Relation rel,
                                 * 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.)
+                                * which is what ATAddCheckNNConstraint wants.)
                                 */
                                if (MergeWithExistingConstraint(rel, ccname, expr,
                                                                                                allow_merge, is_local,
@@ -2481,6 +2541,77 @@ AddRelationNewConstraints(Relation rel,
                        cooked->is_no_inherit = cdef->is_no_inherit;
                        cookedConstraints = lappend(cookedConstraints, cooked);
                }
+               else if (cdef->contype == CONSTR_NOTNULL)
+               {
+                       CookedConstraint *nncooked;
+                       AttrNumber      colnum;
+                       int16           inhcount = is_local ? 0 : 1;
+                       char       *nnname;
+
+                       /* Determine which column to modify */
+                       colnum = get_attnum(RelationGetRelid(rel), strVal(linitial(cdef->keys)));
+                       if (colnum == InvalidAttrNumber)
+                               ereport(ERROR,
+                                               errcode(ERRCODE_UNDEFINED_COLUMN),
+                                               errmsg("column \"%s\" of relation \"%s\" does not exist",
+                                                          strVal(linitial(cdef->keys)), RelationGetRelationName(rel)));
+                       if (colnum < InvalidAttrNumber)
+                               ereport(ERROR,
+                                               errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                               errmsg("cannot add not-null constraint on system column \"%s\"",
+                                                          strVal(linitial(cdef->keys))));
+
+                       /*
+                        * If the column already has a not-null constraint, we don't want
+                        * to add another one; just adjust inheritance status as needed.
+                        */
+                       if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
+                                                                                is_local, cdef->is_no_inherit))
+                               continue;
+
+                       /*
+                        * 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
+                               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,
+                                                               is_local,
+                                                               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 = inhcount;
+                       nncooked->is_no_inherit = cdef->is_no_inherit;
+
+                       cookedConstraints = lappend(cookedConstraints, nncooked);
+               }
        }
 
        /*
@@ -2648,6 +2779,262 @@ MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
        return found;
 }
 
+/*
+ * 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.
+ *
+ * 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;
+
+       /*
+        * 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.
+        *
+        * 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.
+        */
+       for (int outerpos = 0; outerpos < list_length(constraints); outerpos++)
+       {
+               Constraint *constr;
+               AttrNumber      attnum;
+               char       *conname;
+               int                     inhcount = 0;
+
+               constr = list_nth_node(Constraint, constraints, outerpos);
+
+               Assert(constr->contype == CONSTR_NOTNULL);
+
+               attnum = get_attnum(RelationGetRelid(rel),
+                                                       strVal(linitial(constr->keys)));
+               if (attnum == InvalidAttrNumber)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_UNDEFINED_COLUMN),
+                                       errmsg("column \"%s\" of relation \"%s\" does not exist",
+                                                  strVal(linitial(constr->keys)),
+                                                  RelationGetRelationName(rel)));
+               if (attnum < InvalidAttrNumber)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                       errmsg("cannot add not-null constraint on system column \"%s\"",
+                                                  strVal(linitial(constr->keys))));
+
+               /*
+                * A column can only have one not-null constraint, so discard any
+                * additional ones that appear for columns we already saw; but check
+                * that the NO INHERIT flags match.
+                */
+               for (int restpos = outerpos + 1; restpos < list_length(constraints);)
+               {
+                       Constraint *other;
+
+                       other = list_nth_node(Constraint, constraints, restpos);
+                       if (strcmp(strVal(linitial(constr->keys)),
+                                          strVal(linitial(other->keys))) == 0)
+                       {
+                               if (other->is_no_inherit != constr->is_no_inherit)
+                                       ereport(ERROR,
+                                                       errcode(ERRCODE_SYNTAX_ERROR),
+                                                       errmsg("conflicting NO INHERIT declaration for not-null constraint on column \"%s\"",
+                                                                  strVal(linitial(constr->keys))));
+
+                               /*
+                                * Preserve constraint name if one is specified, but raise an
+                                * error if conflicting ones are specified.
+                                */
+                               if (other->conname)
+                               {
+                                       if (!constr->conname)
+                                               constr->conname = pstrdup(other->conname);
+                                       else if (strcmp(constr->conname, other->conname) != 0)
+                                               ereport(ERROR,
+                                                               errcode(ERRCODE_SYNTAX_ERROR),
+                                                               errmsg("conflicting not-null constraint names \"%s\" and \"%s\"",
+                                                                          constr->conname, other->conname));
+                               }
+
+                               /* XXX do we need to verify any other fields? */
+                               constraints = list_delete_nth_cell(constraints, restpos);
+                       }
+                       else
+                               restpos++;
+               }
+
+               /*
+                * Search in the list of inherited constraints for any entries on the
+                * same column; determine an inheritance count from that.  Also, if at
+                * least one parent has a constraint for this column, then we must not
+                * accept a user specification for a NO INHERIT one.  Any constraint
+                * from parents that we process here is deleted from the list: we no
+                * longer need to process it in the loop below.
+                */
+               foreach_ptr(CookedConstraint, old, old_notnulls)
+               {
+                       if (old->attnum == attnum)
+                       {
+                               /*
+                                * If we get a constraint from the parent, having a local NO
+                                * INHERIT one doesn't work.
+                                */
+                               if (constr->is_no_inherit)
+                                       ereport(ERROR,
+                                                       (errcode(ERRCODE_DATATYPE_MISMATCH),
+                                                        errmsg("cannot define not-null constraint on column \"%s\" with NO INHERIT",
+                                                                       strVal(linitial(constr->keys))),
+                                                        errdetail("The column has an inherited not-null constraint.")));
+
+                               inhcount++;
+                               old_notnulls = foreach_delete_current(old_notnulls, old);
+                       }
+               }
+
+               /*
+                * 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_ptr(char, thisname, givennames)
+                       {
+                               if (strcmp(thisname, 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, true,
+                                               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 for that column.  Because multiple
+        * parents could specify a not-null constraint for the same column, we
+        * must count how many there are and set an appropriate inhcount
+        * accordingly, deleting elements we've already processed.
+        *
+        * 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.
+        */
+       for (int outerpos = 0; outerpos < list_length(old_notnulls); outerpos++)
+       {
+               CookedConstraint *cooked;
+               char       *conname = NULL;
+               int                     inhcount = 1;
+
+               cooked = (CookedConstraint *) list_nth(old_notnulls, outerpos);
+               Assert(cooked->contype == CONSTR_NOTNULL);
+               Assert(cooked->name);
+
+               /*
+                * Preserve the first non-conflicting constraint name we come across.
+                */
+               if (conname == NULL)
+                       conname = cooked->name;
+
+               for (int restpos = outerpos + 1; restpos < list_length(old_notnulls);)
+               {
+                       CookedConstraint *other;
+
+                       other = (CookedConstraint *) list_nth(old_notnulls, restpos);
+                       Assert(other->name);
+                       if (other->attnum == cooked->attnum)
+                       {
+                               if (conname == NULL)
+                                       conname = other->name;
+
+                               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_ptr(char, thisname, nnnames)
+                       {
+                               if (strcmp(thisname, 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);
+
+               /* ignore the origin constraint's is_local and inhcount */
+               StoreRelNotNull(rel, conname, cooked->attnum, true,
+                                               false, inhcount, false);
+
+               nncols = lappend_int(nncols, cooked->attnum);
+       }
+
+       return nncols;
+}
+
 /*
  * Update the count of constraints in the relation's pg_class tuple.
  *
index c4145131ce413e10eb66ee66cbb7d9801d4b27f4..76c78c0d184a13e1591786e981013b568e8895f4 100644 (file)
@@ -440,9 +440,8 @@ CREATE VIEW check_constraints AS
     WHERE pg_has_role(coalesce(c.relowner, t.typowner), 'USAGE')
       AND con.contype = 'c'
 
-    UNION
-    -- not-null constraints on domains
-
+    UNION ALL
+    -- not-null constraints
     SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
            rs.nspname::information_schema.sql_identifier AS constraint_schema,
            con.conname::information_schema.sql_identifier AS constraint_name,
@@ -453,24 +452,7 @@ CREATE VIEW check_constraints AS
             LEFT JOIN pg_type t ON t.oid = con.contypid
             LEFT JOIN pg_attribute at ON (con.conrelid = at.attrelid AND con.conkey[1] = at.attnum)
      WHERE pg_has_role(coalesce(c.relowner, t.typowner), 'USAGE'::text)
-       AND con.contype = 'n'
-
-    UNION
-    -- not-null constraints on relations
-
-    SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog,
-           CAST(n.nspname AS sql_identifier) AS constraint_schema,
-           CAST(CAST(n.oid AS text) || '_' || CAST(r.oid AS text) || '_' || CAST(a.attnum AS text) || '_not_null' AS sql_identifier) AS constraint_name, -- XXX
-           CAST(a.attname || ' IS NOT NULL' AS character_data)
-             AS check_clause
-    FROM pg_namespace n, pg_class r, pg_attribute a
-    WHERE n.oid = r.relnamespace
-      AND r.oid = a.attrelid
-      AND a.attnum > 0
-      AND NOT a.attisdropped
-      AND a.attnotnull
-      AND r.relkind IN ('r', 'p')
-      AND pg_has_role(r.relowner, 'USAGE');
+       AND con.contype = 'n';
 
 GRANT SELECT ON check_constraints TO PUBLIC;
 
@@ -839,6 +821,20 @@ CREATE VIEW constraint_column_usage AS
 
         UNION ALL
 
+        /* not-null constraints */
+        SELECT DISTINCT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname
+          FROM pg_namespace nr, pg_class r, pg_attribute a, pg_namespace nc, pg_constraint c
+          WHERE nr.oid = r.relnamespace
+            AND r.oid = a.attrelid
+            AND r.oid = c.conrelid
+            AND a.attnum = c.conkey[1]
+            AND c.connamespace = nc.oid
+            AND c.contype = 'n'
+            AND r.relkind in ('r', 'p')
+            AND not a.attisdropped
+
+        UNION ALL
+
         /* unique/primary key/foreign key constraints */
         SELECT nr.nspname, r.relname, r.relowner, a.attname, nc.nspname, c.conname
           FROM pg_namespace nr, pg_class r, pg_attribute a, pg_namespace nc,
@@ -1839,6 +1835,7 @@ CREATE VIEW table_constraints AS
            CAST(r.relname AS sql_identifier) AS table_name,
            CAST(
              CASE c.contype WHEN 'c' THEN 'CHECK'
+                            WHEN 'n' THEN 'CHECK'
                             WHEN 'f' THEN 'FOREIGN KEY'
                             WHEN 'p' THEN 'PRIMARY KEY'
                             WHEN 'u' THEN 'UNIQUE' END
@@ -1863,38 +1860,6 @@ CREATE VIEW table_constraints AS
           AND c.contype NOT IN ('t', 'x')  -- ignore nonstandard constraints
           AND r.relkind IN ('r', 'p')
           AND (NOT pg_is_other_temp_schema(nr.oid))
-          AND (pg_has_role(r.relowner, 'USAGE')
-               -- SELECT privilege omitted, per SQL standard
-               OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
-               OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES') )
-
-    UNION ALL
-
-    -- not-null constraints
-
-    SELECT CAST(current_database() AS sql_identifier) AS constraint_catalog,
-           CAST(nr.nspname AS sql_identifier) AS constraint_schema,
-           CAST(CAST(nr.oid AS text) || '_' || CAST(r.oid AS text) || '_' || CAST(a.attnum AS text) || '_not_null' AS sql_identifier) AS constraint_name, -- XXX
-           CAST(current_database() AS sql_identifier) AS table_catalog,
-           CAST(nr.nspname AS sql_identifier) AS table_schema,
-           CAST(r.relname AS sql_identifier) AS table_name,
-           CAST('CHECK' AS character_data) AS constraint_type,
-           CAST('NO' AS yes_or_no) AS is_deferrable,
-           CAST('NO' AS yes_or_no) AS initially_deferred,
-           CAST('YES' AS yes_or_no) AS enforced,
-           CAST(NULL AS yes_or_no) AS nulls_distinct
-
-    FROM pg_namespace nr,
-         pg_class r,
-         pg_attribute a
-
-    WHERE nr.oid = r.relnamespace
-          AND r.oid = a.attrelid
-          AND a.attnotnull
-          AND a.attnum > 0
-          AND NOT a.attisdropped
-          AND r.relkind IN ('r', 'p')
-          AND (NOT pg_is_other_temp_schema(nr.oid))
           AND (pg_has_role(r.relowner, 'USAGE')
                -- SELECT privilege omitted, per SQL standard
                OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
index 54f3fb50a5797bf6db246a627b2db497f542bdb9..e953000c01de4ff0fec5051c6302e3f55e3824dd 100644 (file)
@@ -21,6 +21,7 @@
 #include "access/table.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"
@@ -564,6 +565,78 @@ ChooseConstraintName(const char *name1, const char *name2,
        return conname;
 }
 
+/*
+ * Find and return a copy of the pg_constraint tuple that implements a
+ * validated not-null constraint for the given column of the given relation.
+ * If no such constraint exists, return NULL.
+ *
+ * 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.  If
+ * no such column or no such constraint exists, return NULL.
+ */
+HeapTuple
+findNotNullConstraint(Oid relid, const char *colname)
+{
+       AttrNumber      attnum;
+
+       attnum = get_attnum(relid, colname);
+       if (attnum <= InvalidAttrNumber)
+               return NULL;
+
+       return findNotNullConstraintAttnum(relid, attnum);
+}
+
 /*
  * Find and return the pg_constraint tuple that implements a validated
  * not-null constraint for the given domain.
@@ -608,6 +681,185 @@ findDomainNotNullConstraint(Oid typid)
        return retval;
 }
 
+/*
+ * 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));
+       Assert(colnum > 0 && colnum <= MaxAttrNumber);
+
+       if ((Pointer) arr != DatumGetPointer(adatum))
+               pfree(arr);                             /* free de-toasted copy, if any */
+
+       return colnum;
+}
+
+/*
+ * AdjustNotNullInheritance
+ *             Adjust inheritance status for a single not-null constraint
+ *
+ * If no not-null constraint is found for the column, return false.
+ * Caller can create one.
+ * If a constraint exists but the connoinherit flag is not what the caller
+ * wants, throw an error about the incompatibility.  Otherwise, we adjust
+ * conislocal/coninhcount and return true.
+ * In the latter case, if is_local is true we flip conislocal true, or do
+ * nothing if it's already true; otherwise we increment coninhcount by 1.
+ */
+bool
+AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
+                                                bool is_local, bool is_no_inherit)
+{
+       HeapTuple       tup;
+
+       tup = findNotNullConstraintAttnum(relid, attnum);
+       if (HeapTupleIsValid(tup))
+       {
+               Relation        pg_constraint;
+               Form_pg_constraint conform;
+               bool            changed = false;
+
+               pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock);
+               conform = (Form_pg_constraint) GETSTRUCT(tup);
+
+               /*
+                * If the NO INHERIT flag we're asked for doesn't match what the
+                * existing constraint has, throw an error.
+                */
+               if (is_no_inherit != conform->connoinherit)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+                                       errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
+                                                  NameStr(conform->conname), get_rel_name(relid)));
+
+               if (!is_local)
+               {
+                       if (pg_add_s16_overflow(conform->coninhcount, 1,
+                                                                       &conform->coninhcount))
+                               ereport(ERROR,
+                                               errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+                                               errmsg("too many inheritance parents"));
+                       changed = true;
+               }
+               else if (!conform->conislocal)
+               {
+                       conform->conislocal = true;
+                       changed = true;
+               }
+
+               if (changed)
+                       CatalogTupleUpdate(pg_constraint, &tup->t_self, tup);
+
+               table_close(pg_constraint, RowExclusiveLock);
+
+               return true;
+       }
+
+       return false;
+}
+
+/*
+ * 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.
+ *
+ * 'include_noinh' determines whether to include NO INHERIT constraints or not.
+ */
+List *
+RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
+{
+       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 && !include_noinh)
+                       continue;
+
+               colnum = extractNotNullColumn(htup);
+
+               if (cooked)
+               {
+                       CookedConstraint *cooked;
+
+                       cooked = (CookedConstraint *) palloc(sizeof(CookedConstraint));
+
+                       cooked->contype = CONSTR_NOTNULL;
+                       cooked->conoid = conForm->oid;
+                       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;
+                       constr->is_no_inherit = conForm->connoinherit;
+                       notnulls = lappend(notnulls, constr);
+               }
+       }
+
+       systable_endscan(conscan);
+       table_close(constrRel, AccessShareLock);
+
+       return notnulls;
+}
+
+
 /*
  * Delete a single constraint record.
  */
index eaa81424270feaf77c7f5bb310141bd4f79b02bb..ccd9645e7d2d1b3c486af9a713cbb5b87baf8cdd 100644 (file)
@@ -371,7 +371,8 @@ static void truncate_check_activity(Relation rel);
 static void RangeVarCallbackForTruncate(const RangeVar *relation,
                                                                                Oid relId, Oid oldRelId, void *arg);
 static List *MergeAttributes(List *columns, const List *supers, char relpersistence,
-                                                        bool is_partition, List **supconstr);
+                                                        bool is_partition, List **supconstr,
+                                                        List **supnotnulls);
 static List *MergeCheckConstraint(List *constraints, const char *name, Node *expr);
 static void MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const ColumnDef *newdef);
 static ColumnDef *MergeInheritedAttribute(List *inh_columns, int exist_attno, const ColumnDef *newdef);
@@ -456,15 +457,14 @@ static bool check_for_column_name_collision(Relation rel, const char *colname,
                                                                                        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 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 void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
+                                                  LOCKMODE lockmode);
+static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
+                                                                         char *constrname, char *colName,
+                                                                         bool recurse, bool recursing,
+                                                                         LOCKMODE lockmode);
 static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr);
 static bool ConstraintImpliedByRelConstraint(Relation scanrel,
                                                                                         List *testConstraint, List *provenConstraint);
@@ -496,6 +496,9 @@ static ObjectAddress ATExecDropColumn(List **wqueue, Relation rel, const char *c
                                                                          bool recurse, bool recursing,
                                                                          bool missing_ok, LOCKMODE lockmode,
                                                                          ObjectAddresses *addrs);
+static void ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
+                                                               bool recurse, 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,
@@ -507,11 +510,11 @@ static ObjectAddress ATExecAddConstraint(List **wqueue,
 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,
@@ -577,9 +580,12 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
                                                                           Oid *insertTriggerOid,
                                                                           Oid *updateTriggerOid);
 static void ATExecDropConstraint(Relation rel, const char *constrName,
-                                                                DropBehavior behavior,
-                                                                bool recurse, bool recursing,
+                                                                DropBehavior behavior, bool recurse,
                                                                 bool missing_ok, LOCKMODE lockmode);
+static ObjectAddress dropconstraint_internal(Relation rel,
+                                                                                        HeapTuple constraintTup, DropBehavior behavior,
+                                                                                        bool recurse, bool recursing,
+                                                                                        bool missing_ok, LOCKMODE lockmode);
 static void ATPrepAlterColumnType(List **wqueue,
                                                                  AlteredTableInfo *tab, Relation rel,
                                                                  bool recurse, bool recursing,
@@ -677,6 +683,7 @@ static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx,
 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, const char *compression);
@@ -714,8 +721,10 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
        TupleDesc       descriptor;
        List       *inheritOids;
        List       *old_constraints;
+       List       *old_notnulls;
        List       *rawDefaults;
        List       *cookedDefaults;
+       List       *nncols;
        Datum           reloptions;
        ListCell   *listptr;
        AttrNumber      attnum;
@@ -906,12 +915,13 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
                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);
 
@@ -1283,6 +1293,17 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
                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_int(attrnum, nncols)
+               set_attnotnull(NULL, rel, attrnum, NoLock);
+
        ObjectAddressSet(address, RelationRelationId, relationId);
 
        /*
@@ -2414,6 +2435,8 @@ storage_name(char c)
  * 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.
@@ -2444,7 +2467,10 @@ storage_name(char c)
  *
  *        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.
@@ -2463,10 +2489,11 @@ storage_name(char c)
  */
 static List *
 MergeAttributes(List *columns, const List *supers, char relpersistence,
-                               bool is_partition, List **supconstr)
+                               bool is_partition, List **supconstr, List **supnotnulls)
 {
        List       *inh_columns = NIL;
        List       *constraints = NIL;
+       List       *nnconstraints = NIL;
        bool            have_bogus_defaults = false;
        int                     child_attno;
        static Node bogus_marker = {0}; /* marks conflicting defaults */
@@ -2577,8 +2604,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
                AttrMap    *newattmap;
                List       *inherited_defaults;
                List       *cols_with_defaults;
+               List       *nnconstrs;
                ListCell   *lc1;
                ListCell   *lc2;
+               Bitmapset  *nncols = NULL;
 
                /* caller already got lock */
                relation = table_open(parent, NoLock);
@@ -2666,6 +2695,15 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
                /* We can't process inherited defaults until newattmap is complete. */
                inherited_defaults = cols_with_defaults = NIL;
 
+               /*
+                * Request attnotnull on columns that have a not-null constraint
+                * that's not marked NO INHERIT.
+                */
+               nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation),
+                                                                                                 true, false);
+               foreach_ptr(CookedConstraint, cc, nnconstrs)
+                       nncols = bms_add_member(nncols, cc->attnum);
+
                for (AttrNumber parent_attno = 1; parent_attno <= tupleDesc->natts;
                         parent_attno++)
                {
@@ -2687,7 +2725,6 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
                         */
                        newdef = makeColumnDef(attributeName, attribute->atttypid,
                                                                   attribute->atttypmod, attribute->attcollation);
-                       newdef->is_not_null = attribute->attnotnull;
                        newdef->storage = attribute->attstorage;
                        newdef->generated = attribute->attgenerated;
                        if (CompressionMethodIsValid(attribute->attcompression))
@@ -2735,6 +2772,12 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
                                mergeddef = newdef;
                        }
 
+                       /*
+                        * mark attnotnull if parent has it
+                        */
+                       if (bms_is_member(parent_attno, nncols))
+                               mergeddef->is_not_null = true;
+
                        /*
                         * Locate default/generation expression if any
                         */
@@ -2846,6 +2889,19 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
                        }
                }
 
+               /*
+                * Also copy the not-null constraints from this parent.  The
+                * attnotnull markings were already installed above.
+                */
+               foreach_ptr(CookedConstraint, nn, nnconstrs)
+               {
+                       Assert(nn->contype == CONSTR_NOTNULL);
+
+                       nn->attnum = newattmap->attnums[nn->attnum - 1];
+
+                       nnconstraints = lappend(nnconstraints, nn);
+               }
+
                free_attrmap(newattmap);
 
                /*
@@ -2916,8 +2972,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
        /*
         * 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)
        {
@@ -2934,7 +2989,6 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
                                if (strcmp(coldef->colname, restdef->colname) == 0)
                                {
                                        found = true;
-                                       coldef->is_not_null |= restdef->is_not_null;
 
                                        /*
                                         * Check for conflicts related to generated columns.
@@ -3023,6 +3077,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
        }
 
        *supconstr = constraints;
+       *supnotnulls = nnconstraints;
 
        return columns;
 }
@@ -3303,11 +3358,6 @@ MergeInheritedAttribute(List *inh_columns,
                                                   format_type_with_typemod(prevtypeid, prevtypmod),
                                                   format_type_with_typemod(newtypeid, newtypmod))));
 
-       /*
-        * Merge of not-null constraints = OR 'em together
-        */
-       prevdef->is_not_null |= newdef->is_not_null;
-
        /*
         * Must have the same collation
         */
@@ -3946,7 +3996,10 @@ rename_constraint_internal(Oid myrelid,
                         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)
                {
@@ -4704,15 +4757,6 @@ AlterTableGetLockLevel(List *cmds)
                                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);
@@ -4881,22 +4925,17 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
                case AT_DropNotNull:    /* ALTER COLUMN DROP NOT NULL */
                        ATSimplePermissions(cmd->subtype, rel,
                                                                ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
-                       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_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
-                       /* Need command-specific recursion decision */
-                       ATPrepSetNotNull(wqueue, rel, cmd, recurse, recursing,
-                                                        lockmode, context);
-                       pass = AT_PASS_COL_ATTRS;
-                       break;
-               case AT_CheckNotNull:   /* check column is already marked NOT NULL */
-                       ATSimplePermissions(cmd->subtype, rel,
-                                                               ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
-                       ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
-                       /* No command-specific prep needed */
+                       /* Set up recursion for phase 2; no other prep needed */
+                       if (recurse)
+                               cmd->recurse = true;
                        pass = AT_PASS_COL_ATTRS;
                        break;
                case AT_SetExpression:  /* ALTER COLUMN SET EXPRESSION */
@@ -4961,10 +5000,13 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
                case AT_AddConstraint:  /* ADD CONSTRAINT */
                        ATSimplePermissions(cmd->subtype, rel,
                                                                ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
-                       /* Recursion occurs during execution phase */
-                       /* No command-specific prep needed except saving recurse flag */
+                       ATPrepAddPrimaryKey(wqueue, rel, cmd, recurse, lockmode, context);
                        if (recurse)
+                       {
+                               /* recurses at exec time; lock descendants and set flag */
+                               (void) find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
                                cmd->recurse = true;
+                       }
                        pass = AT_PASS_ADD_CONSTR;
                        break;
                case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
@@ -5279,13 +5321,11 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
                        address = ATExecDropIdentity(rel, cmd->name, cmd->missing_ok, lockmode, cmd->recurse, false);
                        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);
-                       break;
-               case AT_CheckNotNull:   /* check column is already marked NOT NULL */
-                       ATExecCheckNotNull(tab, rel, cmd->name, lockmode);
+                       address = ATExecSetNotNull(wqueue, rel, NULL, cmd->name,
+                                                                          cmd->recurse, false, lockmode);
                        break;
                case AT_SetExpression:
                        address = ATExecSetExpression(tab, rel, cmd->name, cmd->def, lockmode);
@@ -5368,7 +5408,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
                        break;
                case AT_DropConstraint: /* DROP CONSTRAINT */
                        ATExecDropConstraint(rel, cmd->name, cmd->behavior,
-                                                                cmd->recurse, false,
+                                                                cmd->recurse,
                                                                 cmd->missing_ok, lockmode);
                        break;
                case AT_AlterColumnType:        /* ALTER COLUMN TYPE */
@@ -5631,21 +5671,10 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
                 */
                switch (cmd2->subtype)
                {
-                       case AT_SetNotNull:
-                               /* Need command-specific recursion decision */
-                               ATPrepSetNotNull(wqueue, rel, cmd2,
-                                                                recurse, false,
-                                                                lockmode, context);
-                               pass = AT_PASS_COL_ATTRS;
-                               break;
                        case AT_AddIndex:
-                               /* This command never recurses */
-                               /* No command-specific prep needed */
                                pass = AT_PASS_ADD_INDEX;
                                break;
                        case AT_AddIndexConstraint:
-                               /* This command never recurses */
-                               /* No command-specific prep needed */
                                pass = AT_PASS_ADD_INDEXCONSTR;
                                break;
                        case AT_AddConstraint:
@@ -5654,6 +5683,9 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
                                        cmd2->recurse = true;
                                switch (castNode(Constraint, cmd2->def)->contype)
                                {
+                                       case CONSTR_NOTNULL:
+                                               pass = AT_PASS_COL_ATTRS;
+                                               break;
                                        case CONSTR_PRIMARY:
                                        case CONSTR_UNIQUE:
                                        case CONSTR_EXCLUSION:
@@ -6093,8 +6125,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
                /*
                 * If we are rebuilding the tuples OR if we added any new but not
                 * 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.
+                * is a bit of overkill but it minimizes risk of bugs.
                 */
                for (i = 0; i < newTupDesc->natts; i++)
                {
@@ -6314,6 +6345,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode)
                                                                                        RelationGetRelationName(oldrel)),
                                                                         errtableconstraint(oldrel, con->name)));
                                                break;
+                                       case CONSTR_NOTNULL:
                                        case CONSTR_FOREIGN:
                                                /* Nothing to do here */
                                                break;
@@ -6427,8 +6459,6 @@ alter_table_type_to_string(AlterTableType cmdtype)
                        return "ALTER COLUMN ... SET EXPRESSION";
                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:
@@ -7524,13 +7554,14 @@ add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
  * 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;
        ObjectAddress address;
 
        /*
@@ -7546,6 +7577,15 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode)
                                                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)
@@ -7561,60 +7601,8 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode)
                                                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.
+        * If rel is partition, shouldn't drop NOT NULL if parent has the same.
         */
-
-       /* Loop over all indexes on the relation */
-       indexoidlist = RelationGetIndexList(rel);
-
-       foreach_oid(indexoid, indexoidlist)
-       {
-               HeapTuple       indexTuple;
-               Form_pg_index indexStruct;
-
-               indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
-               if (!HeapTupleIsValid(indexTuple))
-                       elog(ERROR, "cache lookup failed for index %u", indexoid);
-               indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
-
-               /*
-                * If the index is not a primary key or an index used as replica
-                * identity, skip the check.
-                */
-               if (indexStruct->indisprimary || indexStruct->indisreplident)
-               {
-                       /*
-                        * 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 (int 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)));
-                               }
-                       }
-               }
-
-               ReleaseSysCache(indexTuple);
-       }
-
-       list_free(indexoidlist);
-
-       /* 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);
@@ -7632,19 +7620,18 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode)
        }
 
        /*
-        * Okay, actually perform the catalog change ... if needed
+        * Find the constraint that makes this column NOT NULL, and drop it.
+        * dropconstraint_internal() resets attnotnull.
         */
-       if (attTup->attnotnull)
-       {
-               attTup->attnotnull = false;
+       conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+       if (conTup == NULL)
+               elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+                        colName, RelationGetRelationName(rel));
 
-               CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
-
-               ObjectAddressSubSet(address, RelationRelationId,
-                                                       RelationGetRelid(rel), attnum);
-       }
-       else
-               address = InvalidObjectAddress;
+       /* The normal case: we have a pg_constraint row, remove it */
+       dropconstraint_internal(rel, conTup, DROP_RESTRICT, recurse, false,
+                                                       false, lockmode);
+       heap_freetuple(conTup);
 
        InvokeObjectPostAlterHook(RelationRelationId,
                                                          RelationGetRelid(rel), attnum);
@@ -7655,104 +7642,105 @@ ATExecDropNotNull(Relation rel, const char *colName, LOCKMODE lockmode)
 }
 
 /*
- * 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.
+ *
+ * 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.
  */
-
 static void
-ATPrepSetNotNull(List **wqueue, Relation rel,
-                                AlterTableCmd *cmd, bool recurse, bool recursing,
-                                LOCKMODE lockmode, AlterTableUtilityContext *context)
+set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
+                          LOCKMODE lockmode)
 {
+       Form_pg_attribute attr;
+
+       CheckAlterTableIsSafe(rel);
+
        /*
-        * If we're already recursing, there's nothing to do; the topmost
-        * invocation of ATSimpleRecursion already visited all children.
+        * Exit quickly by testing attnotnull from the tupledesc's copy of the
+        * attribute.
         */
-       if (recursing)
+       attr = TupleDescAttr(RelationGetDescr(rel), attnum - 1);
+       if (attr->attisdropped)
                return;
 
-       /*
-        * 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)
+       if (!attr->attnotnull)
        {
+               Relation        attr_rel;
                HeapTuple       tuple;
-               bool            attnotnull;
 
-               tuple = SearchSysCacheAttName(RelationGetRelid(rel), cmd->name);
+               attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
 
-               /* Might as well throw the error now, if name is bad */
+               tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
                if (!HeapTupleIsValid(tuple))
-                       ereport(ERROR,
-                                       (errcode(ERRCODE_UNDEFINED_COLUMN),
-                                        errmsg("column \"%s\" of relation \"%s\" does not exist",
-                                                       cmd->name, RelationGetRelationName(rel))));
+                       elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+                                attnum, RelationGetRelid(rel));
 
-               attnotnull = ((Form_pg_attribute) GETSTRUCT(tuple))->attnotnull;
-               ReleaseSysCache(tuple);
-               if (attnotnull)
-                       return;
-       }
+               attr = (Form_pg_attribute) GETSTRUCT(tuple);
+               Assert(!attr->attnotnull);
+               attr->attnotnull = true;
+               CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
 
-       /*
-        * 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)
-       {
-               AlterTableCmd *newcmd = makeNode(AlterTableCmd);
+               /*
+                * If the nullness isn't already proven by validated constraints, have
+                * ALTER TABLE phase 3 test for it.
+                */
+               if (wqueue && !NotNullImpliedByRelConstraints(rel, attr))
+               {
+                       AlteredTableInfo *tab;
+
+                       tab = ATGetQueueEntry(wqueue, rel);
+                       tab->verify_new_notnull = true;
+               }
+
+               CommandCounterIncrement();
 
-               newcmd->subtype = AT_CheckNotNull;
-               newcmd->name = pstrdup(cmd->name);
-               ATSimpleRecursion(wqueue, rel, newcmd, true, lockmode, context);
+               table_close(attr_rel, RowExclusiveLock);
+               heap_freetuple(tuple);
        }
-       else
-               ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
 }
 
 /*
- * 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, LOCKMODE lockmode)
 {
        HeapTuple       tuple;
-       Form_pg_attribute attTup;
        AttrNumber      attnum;
-       Relation        attr_rel;
        ObjectAddress address;
+       Constraint *constraint;
+       CookedConstraint *ccon;
+       List       *cooked;
+       bool            is_no_inherit = false;
 
-       /*
-        * lookup the attribute
-        */
-       attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
+       /* Guard against stack overflow due to overly deep inheritance tree. */
+       check_stack_depth();
 
-       tuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), colName);
+       /* At top level, permission check was done in ATPrepCmd, else do it */
+       if (recursing)
+       {
+               ATSimplePermissions(AT_AddConstraint, rel,
+                                                       ATT_PARTITIONED_TABLE | 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))));
 
-       attTup = (Form_pg_attribute) GETSTRUCT(tuple);
-       attnum = attTup->attnum;
-
        /* Prevent them from altering a system attribute */
        if (attnum <= 0)
                ereport(ERROR,
@@ -7760,79 +7748,130 @@ ATExecSetNotNull(AlteredTableInfo *tab, Relation rel,
                                 errmsg("cannot alter system column \"%s\"",
                                                colName)));
 
-       /*
-        * Okay, actually perform the catalog change ... if needed
-        */
-       if (!attTup->attnotnull)
+       /* See if there's already a constraint */
+       tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+       if (HeapTupleIsValid(tuple))
        {
-               attTup->attnotnull = true;
+               Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+               bool            changed = false;
 
-               CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+               /*
+                * Don't let a NO INHERIT constraint be changed into inherit.
+                */
+               if (conForm->connoinherit && recurse)
+                       ereport(ERROR,
+                                       errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                       errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
+                                                  NameStr(conForm->conname),
+                                                  RelationGetRelationName(rel)));
 
                /*
-                * 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, attTup))
+               if (recursing)
                {
-                       /* Tell Phase 3 it needs to test the constraint */
-                       tab->verify_new_notnull = true;
+                       if (pg_add_s16_overflow(conForm->coninhcount, 1,
+                                                                       &conForm->coninhcount))
+                               ereport(ERROR,
+                                               errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+                                               errmsg("too many inheritance parents"));
+                       changed = true;
+               }
+               else if (!conForm->conislocal)
+               {
+                       conForm->conislocal = true;
+                       changed = true;
                }
 
-               ObjectAddressSubSet(address, RelationRelationId,
-                                                       RelationGetRelid(rel), attnum);
+               if (changed)
+               {
+                       Relation        constr_rel;
+
+                       constr_rel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+                       CatalogTupleUpdate(constr_rel, &tuple->t_self, tuple);
+                       ObjectAddressSet(address, ConstraintRelationId, conForm->oid);
+                       table_close(constr_rel, RowExclusiveLock);
+               }
+
+               if (changed)
+                       return address;
+               else
+                       return InvalidObjectAddress;
+       }
+
+       /*
+        * 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 = makeNotNullConstraint(makeString(colName));
+       constraint->is_no_inherit = is_no_inherit;
+       constraint->conname = conName;
+
+       /* 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 */
+       set_attnotnull(wqueue, rel, attnum, lockmode);
 
-       return address;
-}
+       /*
+        * Recurse to propagate the constraint to children that don't have one.
+        */
+       if (recurse)
+       {
+               List       *children;
 
-/*
- * ALTER TABLE ALTER COLUMN CHECK NOT NULL
- *
- * 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.
- */
-static void
-ATExecCheckNotNull(AlteredTableInfo *tab, Relation rel,
-                                  const char *colName, LOCKMODE lockmode)
-{
-       HeapTuple       tuple;
+               children = find_inheritance_children(RelationGetRelid(rel),
+                                                                                        lockmode);
 
-       tuple = SearchSysCacheAttName(RelationGetRelid(rel), colName);
+               foreach_oid(childoid, children)
+               {
+                       Relation        childrel = table_open(childoid, NoLock);
 
-       if (!HeapTupleIsValid(tuple))
-               ereport(ERROR,
-                               errcode(ERRCODE_UNDEFINED_COLUMN),
-                               errmsg("column \"%s\" of relation \"%s\" does not exist",
-                                          colName, RelationGetRelationName(rel)));
+                       CommandCounterIncrement();
 
-       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.")));
+                       ATExecSetNotNull(wqueue, childrel, conName, colName,
+                                                        recurse, true, lockmode);
+                       table_close(childrel, NoLock);
+               }
+       }
 
-       ReleaseSysCache(tuple);
+       return address;
 }
 
 /*
@@ -9139,6 +9178,71 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
        return object;
 }
 
+/*
+ * Prepare to add a primary key on table, by adding not-null constraints
+ * on all columns.
+ */
+static void
+ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
+                                       bool recurse, LOCKMODE lockmode,
+                                       AlterTableUtilityContext *context)
+{
+       ListCell   *lc;
+       Constraint *pkconstr;
+
+       pkconstr = castNode(Constraint, cmd->def);
+       if (pkconstr->contype != CONSTR_PRIMARY)
+               return;
+
+       /*
+        * If not recursing, we must ensure that all children have a NOT NULL
+        * constraint on the columns, and error out if not.
+        */
+       if (!recurse)
+       {
+               List       *children;
+
+               children = find_inheritance_children(RelationGetRelid(rel),
+                                                                                        lockmode);
+               foreach_oid(childrelid, children)
+               {
+                       foreach(lc, pkconstr->keys)
+                       {
+                               HeapTuple       tup;
+                               Form_pg_attribute attrForm;
+                               char       *attname = strVal(lfirst(lc));
+
+                               tup = SearchSysCacheAttName(childrelid, attname);
+                               if (!tup)
+                                       elog(ERROR, "cache lookup failed for attribute %s of relation %u",
+                                                attname, childrelid);
+                               attrForm = (Form_pg_attribute) GETSTRUCT(tup);
+                               if (!attrForm->attnotnull)
+                                       ereport(ERROR,
+                                                       errmsg("column \"%s\" of table \"%s\" is not marked NOT NULL",
+                                                                  attname, get_rel_name(childrelid)));
+                               ReleaseSysCache(tup);
+                       }
+               }
+       }
+
+       /* Insert not-null constraints in the queue for the PK columns */
+       foreach(lc, pkconstr->keys)
+       {
+               AlterTableCmd *newcmd;
+               Constraint *nnconstr;
+
+               nnconstr = makeNotNullConstraint(lfirst(lc));
+
+               newcmd = makeNode(AlterTableCmd);
+               newcmd->subtype = AT_AddConstraint;
+               newcmd->recurse = true;
+               newcmd->def = (Node *) nnconstr;
+
+               ATPrepCmd(wqueue, rel, newcmd, true, false, lockmode, context);
+       }
+}
+
 /*
  * ALTER TABLE ADD INDEX
  *
@@ -9334,17 +9438,18 @@ ATExecAddConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
        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:
@@ -9425,9 +9530,9 @@ ChooseForeignKeyConstraintNameAddition(List *colnames)
 }
 
 /*
- * 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.
  *
@@ -9440,9 +9545,9 @@ ChooseForeignKeyConstraintNameAddition(List *colnames)
  * 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;
@@ -9450,6 +9555,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
        ListCell   *child;
        ObjectAddress address = InvalidObjectAddress;
 
+       /* Guard against stack overflow due to overly deep inheritance tree. */
+       check_stack_depth();
+
        /* At top level, permission check was done in ATPrepCmd, else do it */
        if (recursing)
                ATSimplePermissions(AT_AddConstraint, rel,
@@ -9481,7 +9589,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
        {
                CookedConstraint *ccon = (CookedConstraint *) lfirst(lcon);
 
-               if (!ccon->skip_validation)
+               if (!ccon->skip_validation && ccon->contype != CONSTR_NOTNULL)
                {
                        NewConstraint *newcon;
 
@@ -9497,11 +9605,18 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
                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, 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();
@@ -9531,7 +9646,7 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 
        /*
         * Check if ONLY was specified with ALTER TABLE.  If so, allow the
-        * constraint creation only if there are no children currently.  Error out
+        * constraint creation only if there are no children currently. Error out
         * otherwise.
         */
        if (!recurse && children != NIL)
@@ -9539,6 +9654,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
                                (errcode(ERRCODE_INVALID_TABLE_DEFINITION),
                                 errmsg("constraint must be added to child tables too")));
 
+       /*
+        * Recurse to create the constraint on each child.
+        */
        foreach(child, children)
        {
                Oid                     childrelid = lfirst_oid(child);
@@ -9552,9 +9670,9 @@ ATAddCheckConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
                /* 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 this child */
+               ATAddCheckNNConstraint(wqueue, childtab, childrel,
+                                                          constr, recurse, true, is_readd, lockmode);
 
                table_close(childrel, NoLock);
        }
@@ -12667,24 +12785,14 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
  */
 static void
 ATExecDropConstraint(Relation rel, const char *constrName,
-                                        DropBehavior behavior,
-                                        bool recurse, bool recursing,
+                                        DropBehavior behavior, bool recurse,
                                         bool missing_ok, LOCKMODE lockmode)
 {
-       List       *children;
        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)
-               ATSimplePermissions(AT_DropConstraint, rel,
-                                                       ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
 
        conrel = table_open(ConstraintRelationId, RowExclusiveLock);
 
@@ -12709,80 +12817,190 @@ ATExecDropConstraint(Relation rel, const char *constrName,
        /* There can be at most one matching row */
        if (HeapTupleIsValid(tuple = systable_getnext(scan)))
        {
-               ObjectAddress conobj;
+               dropconstraint_internal(rel, tuple, behavior, recurse, false,
+                                                               missing_ok, 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);
+}
+
+/*
+ * 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,
+                                               LOCKMODE lockmode)
+{
+       Relation        conrel;
+       Form_pg_constraint con;
+       ObjectAddress conobj;
+       List       *children;
+       bool            is_no_inherit_constraint = false;
+       char       *constrName;
+       char       *colname = NULL;
+
+       /* Guard against stack overflow due to overly deep inheritance tree. */
+       check_stack_depth();
+
+       /* At top level, permission check was done in ATPrepCmd, else do it */
+       if (recursing)
+               ATSimplePermissions(AT_DropConstraint, rel,
+                                                       ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
+
+       conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+
+       con = (Form_pg_constraint) GETSTRUCT(constraintTup);
+       constrName = NameStr(con->conname);
+
+       /* 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))));
+
+       /*
+        * Reset pg_constraint.attnotnull, if this is a not-null constraint.
+        *
+        * While doing that, we're in a good position to disallow dropping a not-
+        * null constraint underneath a primary key, a replica identity index, or
+        * a generated identity column.
+        */
+       if (con->contype == CONSTRAINT_NOTNULL)
+       {
+               Relation        attrel = table_open(AttributeRelationId, RowExclusiveLock);
+               AttrNumber      attnum = extractNotNullColumn(constraintTup);
+               Bitmapset  *pkattrs;
+               Bitmapset  *irattrs;
+               HeapTuple       atttup;
+               Form_pg_attribute attForm;
+
+               /* save column name for recursion step */
+               colname = get_attname(RelationGetRelid(rel), attnum, false);
 
                /*
-                * 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.
+                * Disallow if it's in the primary key.  For partitioned tables we
+                * cannot rely solely on RelationGetIndexAttrBitmap, because it'll
+                * return NULL if the primary key is invalid; but we still need to
+                * protect not-null constraints under such a constraint, so check the
+                * slow way.
                 */
-               if (contype == CONSTRAINT_FOREIGN &&
-                       con->confrelid != RelationGetRelid(rel))
+               pkattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY);
+
+               if (pkattrs == NULL &&
+                       rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
                {
-                       Relation        frel;
+                       Oid                     pkindex = RelationGetPrimaryKeyIndex(rel, true);
+
+                       if (OidIsValid(pkindex))
+                       {
+                               Relation        pk = relation_open(pkindex, AccessShareLock);
+
+                               pkattrs = NULL;
+                               for (int i = 0; i < pk->rd_index->indnkeyatts; i++)
+                                       pkattrs = bms_add_member(pkattrs, pk->rd_index->indkey.values[i]);
 
-                       /* Must match lock taken by RemoveTriggerById: */
-                       frel = table_open(con->confrelid, AccessExclusiveLock);
-                       CheckAlterTableIsSafe(frel);
-                       table_close(frel, NoLock);
+                               relation_close(pk, AccessShareLock);
+                       }
                }
 
-               /*
-                * Perform the actual constraint deletion
-                */
-               conobj.classId = ConstraintRelationId;
-               conobj.objectId = con->oid;
-               conobj.objectSubId = 0;
+               if (pkattrs &&
+                       bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, pkattrs))
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                                       errmsg("column \"%s\" is in a primary key",
+                                                  get_attname(RelationGetRelid(rel), attnum, false)));
+
+               /* Disallow if it's in the replica identity */
+               irattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY);
+               if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, irattrs))
+                       ereport(ERROR,
+                                       errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+                                       errmsg("column \"%s\" is in index used as replica identity",
+                                                  get_attname(RelationGetRelid(rel), attnum, false)));
+
+               /* Disallow if it's a GENERATED AS IDENTITY column */
+               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);
+               if (attForm->attidentity != '\0')
+                       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)));
 
-               performDeletion(&conobj, behavior, 0);
+               /* All good -- reset attnotnull if needed */
+               if (attForm->attnotnull)
+               {
+                       attForm->attnotnull = false;
+                       CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
+               }
 
-               found = true;
+               table_close(attrel, RowExclusiveLock);
        }
 
-       systable_endscan(scan);
+       is_no_inherit_constraint = con->connoinherit;
 
-       if (!found)
+       /*
+        * 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
-               {
-                       ereport(NOTICE,
-                                       (errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping",
-                                                       constrName, RelationGetRelationName(rel))));
-                       table_close(conrel, RowExclusiveLock);
-                       return;
-               }
+               Relation        frel;
+
+               /* Must match lock taken by RemoveTriggerById: */
+               frel = table_open(con->confrelid, AccessExclusiveLock);
+               CheckAlterTableIsSafe(frel);
+               table_close(frel, NoLock);
        }
 
        /*
-        * For partitioned tables, non-CHECK inherited constraints are dropped via
-        * the dependency mechanism, so we're done here.
+        * Perform the actual constraint deletion
         */
-       if (contype != CONSTRAINT_CHECK &&
+       ObjectAddressSet(conobj, ConstraintRelationId, con->oid);
+       performDeletion(&conobj, behavior, 0);
+
+       /*
+        * For partitioned tables, non-CHECK, non-NOT-NULL inherited constraints
+        * are dropped via the dependency mechanism, so we're done here.
+        */
+       if (con->contype != CONSTRAINT_CHECK &&
+               con->contype != CONSTRAINT_NOTNULL &&
                rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
        {
                table_close(conrel, RowExclusiveLock);
-               return;
+               return conobj;
        }
 
        /*
@@ -12798,48 +13016,65 @@ ATExecDropConstraint(Relation rel, const char *constrName,
        foreach_oid(childrelid, children)
        {
                Relation        childrel;
-               HeapTuple       copy_tuple;
+               HeapTuple       tuple;
+               Form_pg_constraint childcon;
 
                /* find_inheritance_children already got lock */
                childrel = table_open(childrelid, NoLock);
                CheckAlterTableIsSafe(childrel);
 
-               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 constraints by column name, and others by
+                * constraint 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)
                {
@@ -12847,18 +13082,18 @@ ATExecDropConstraint(Relation rel, const char *constrName,
                         * 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,
+                                                                               lockmode);
                        }
                        else
                        {
                                /* Child constraint must survive my deletion */
-                               con->coninhcount--;
-                               CatalogTupleUpdate(conrel, &copy_tuple->t_self, copy_tuple);
+                               childcon->coninhcount--;
+                               CatalogTupleUpdate(conrel, &tuple->t_self, tuple);
 
                                /* Make update visible */
                                CommandCounterIncrement();
@@ -12867,25 +13102,29 @@ ATExecDropConstraint(Relation rel, const char *constrName,
                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, &copy_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);
        }
 
        table_close(conrel, RowExclusiveLock);
+
+       return conobj;
 }
 
 /*
@@ -13834,10 +14073,26 @@ RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab)
                char       *defstring = pg_get_constraintdef_command(conoid);
                Oid                     indoid;
 
-               tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids,
-                                                                                                conoid);
-               tab->changedConstraintDefs = lappend(tab->changedConstraintDefs,
-                                                                                        defstring);
+               /*
+                * It is critical to create not-null constraints ahead of primary key
+                * indexes; otherwise, the not-null constraint would be created by the
+                * primary key, and the constraint name would be wrong.
+                */
+               if (get_constraint_type(conoid) == CONSTRAINT_NOTNULL)
+               {
+                       tab->changedConstraintOids = lcons_oid(conoid,
+                                                                                                  tab->changedConstraintOids);
+                       tab->changedConstraintDefs = lcons(defstring,
+                                                                                          tab->changedConstraintDefs);
+               }
+               else
+               {
+
+                       tab->changedConstraintOids = lappend_oid(tab->changedConstraintOids,
+                                                                                                        conoid);
+                       tab->changedConstraintDefs = lappend(tab->changedConstraintDefs,
+                                                                                                defstring);
+               }
 
                /*
                 * For the index of a constraint, if any, remember if it is used for
@@ -14000,9 +14255,10 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 
                /*
                 * 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;
@@ -14241,23 +14497,21 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
                                        tab->subcmds[AT_PASS_OLD_CONSTR] =
                                                lappend(tab->subcmds[AT_PASS_OLD_CONSTR], cmd);
 
-                                       /* recreate any comment on the constraint */
-                                       RebuildConstraintComment(tab,
-                                                                                        AT_PASS_OLD_CONSTR,
-                                                                                        oldId,
-                                                                                        rel,
-                                                                                        NIL,
-                                                                                        con->conname);
-                               }
-                               else if (cmd->subtype == AT_SetNotNull)
-                               {
                                        /*
-                                        * The parser will create AT_SetNotNull 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
-                                        * the new table definition.
+                                        * Recreate any comment on the constraint.  If we have
+                                        * recreated a primary key, then transformTableConstraint
+                                        * has added an unnamed not-null constraint here; skip
+                                        * this in that case.
                                         */
+                                       if (con->conname)
+                                               RebuildConstraintComment(tab,
+                                                                                                AT_PASS_OLD_CONSTR,
+                                                                                                oldId,
+                                                                                                rel,
+                                                                                                NIL,
+                                                                                                con->conname);
+                                       else
+                                               Assert(con->contype == CONSTR_NOTNULL);
                                }
                                else
                                        elog(ERROR, "unexpected statement subtype: %d",
@@ -16012,14 +16266,24 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
                                                                RelationGetRelationName(child_rel), parent_attname)));
 
                        /*
-                        * Check child doesn't discard NOT NULL property.  (Other
-                        * constraints are checked elsewhere.)
+                        * If the parent has a not-null constraint that's not NO INHERIT,
+                        * make sure the child has one too.
+                        *
+                        * Other constraints are checked elsewhere.
                         */
                        if (parent_att->attnotnull && !child_att->attnotnull)
-                               ereport(ERROR,
-                                               (errcode(ERRCODE_DATATYPE_MISMATCH),
-                                                errmsg("column \"%s\" in child table must be marked NOT NULL",
-                                                               parent_attname)));
+                       {
+                               HeapTuple       contup;
+
+                               contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
+                                                                                                        parent_att->attnum);
+                               if (HeapTupleIsValid(contup) &&
+                                       !((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
+                                       ereport(ERROR,
+                                                       errcode(ERRCODE_DATATYPE_MISMATCH),
+                                                       errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL",
+                                                                  parent_attname, RelationGetRelationName(child_rel)));
+                       }
 
                        /*
                         * Child column must be generated if and only if parent column is.
@@ -16101,6 +16365,7 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
        ScanKeyData parent_key;
        HeapTuple       parent_tuple;
        Oid                     parent_relid = RelationGetRelid(parent_rel);
+       AttrMap    *attmap;
 
        constraintrel = table_open(ConstraintRelationId, RowExclusiveLock);
 
@@ -16112,21 +16377,32 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
        parent_scan = systable_beginscan(constraintrel, ConstraintRelidTypidNameIndexId,
                                                                         true, NULL, 1, &parent_key);
 
+       attmap = build_attrmap_by_name(RelationGetDescr(parent_rel),
+                                                                  RelationGetDescr(child_rel),
+                                                                  true);
+
        while (HeapTupleIsValid(parent_tuple = systable_getnext(parent_scan)))
        {
                Form_pg_constraint parent_con = (Form_pg_constraint) GETSTRUCT(parent_tuple);
                SysScanDesc child_scan;
                ScanKeyData child_key;
                HeapTuple       child_tuple;
+               AttrNumber      parent_attno;
                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 */
                if (parent_con->connoinherit)
                        continue;
 
+               if (parent_con->contype == CONSTRAINT_NOTNULL)
+                       parent_attno = extractNotNullColumn(parent_tuple);
+               else
+                       parent_attno = InvalidAttrNumber;
+
                /* Search for a child constraint matching this one */
                ScanKeyInit(&child_key,
                                        Anum_pg_constraint_conrelid,
@@ -16140,20 +16416,46 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
                        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),
-                                          NameStr(child_con->conname)) != 0)
-                               continue;
+                       /*
+                        * CHECK constraint are matched by constraint name, NOT NULL ones
+                        * by attribute number.
+                        */
+                       if (child_con->contype == CONSTRAINT_CHECK)
+                       {
+                               if (strcmp(NameStr(parent_con->conname),
+                                                  NameStr(child_con->conname)) != 0)
+                                       continue;
+                       }
+                       else if (child_con->contype == CONSTRAINT_NOTNULL)
+                       {
+                               Form_pg_attribute parent_attr;
+                               Form_pg_attribute child_attr;
+                               AttrNumber      child_attno;
+
+                               parent_attr = TupleDescAttr(parent_rel->rd_att, parent_attno - 1);
+                               child_attno = extractNotNullColumn(child_tuple);
+                               if (parent_attno != attmap->attnums[child_attno - 1])
+                                       continue;
 
-                       if (!constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel)))
+                               child_attr = TupleDescAttr(child_rel->rd_att, child_attno - 1);
+                               /* there shouldn't be constraints on dropped columns */
+                               if (parent_attr->attisdropped || child_attr->attisdropped)
+                                       elog(ERROR, "found not-null constraint on dropped columns");
+                       }
+
+                       if (child_con->contype == CONSTRAINT_CHECK &&
+                               !constraints_equivalent(parent_tuple, child_tuple, RelationGetDescr(constraintrel)))
                                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 the child constraint is "no inherit" then cannot merge
+                        */
                        if (child_con->connoinherit)
                                ereport(ERROR,
                                                (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -16204,10 +16506,21 @@ MergeConstraintsIntoExisting(Relation child_rel, Relation parent_rel)
                systable_endscan(child_scan);
 
                if (!found)
+               {
+                       if (parent_con->contype == CONSTRAINT_NOTNULL)
+                               ereport(ERROR,
+                                               errcode(ERRCODE_DATATYPE_MISMATCH),
+                                               errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL",
+                                                          get_attname(parent_relid,
+                                                                                  extractNotNullColumn(parent_tuple),
+                                                                                  false),
+                                                          RelationGetRelationName(child_rel)));
+
                        ereport(ERROR,
                                        (errcode(ERRCODE_DATATYPE_MISMATCH),
                                         errmsg("child table is missing constraint \"%s\"",
                                                        NameStr(parent_con->conname))));
+               }
        }
 
        systable_endscan(parent_scan);
@@ -16352,7 +16665,9 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached)
        ScanKeyData key[3];
        HeapTuple       attributeTuple,
                                constraintTuple;
+       AttrMap    *attmap;
        List       *connames;
+       List       *nncolumns;
        bool            found;
        bool            is_partitioning;
 
@@ -16417,11 +16732,18 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached)
        table_close(catalogRelation, RowExclusiveLock);
 
        /*
-        * Likewise, find inherited check constraints and disinherit them. To do
-        * 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,
+        * Likewise, find inherited check and not-null constraints and disinherit
+        * them. To do 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, mapping them in
+        * to the child rel's attribute numbers.
         */
+       attmap = build_attrmap_by_name(RelationGetDescr(child_rel),
+                                                                  RelationGetDescr(parent_rel),
+                                                                  false);
+
        catalogRelation = table_open(ConstraintRelationId, RowExclusiveLock);
        ScanKeyInit(&key[0],
                                Anum_pg_constraint_conrelid,
@@ -16431,18 +16753,28 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached)
                                                          true, NULL, 1, key);
 
        connames = NIL;
+       nncolumns = NIL;
 
        while (HeapTupleIsValid(constraintTuple = systable_getnext(scan)))
        {
                Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple);
 
+               if (con->connoinherit)
+                       continue;
+
                if (con->contype == CONSTRAINT_CHECK)
                        connames = lappend(connames, pstrdup(NameStr(con->conname)));
+               if (con->contype == CONSTRAINT_NOTNULL)
+               {
+                       AttrNumber      parent_attno = extractNotNullColumn(constraintTuple);
+
+                       nncolumns = lappend_int(nncolumns, attmap->attnums[parent_attno - 1]);
+               }
        }
 
        systable_endscan(scan);
 
-       /* Now scan the child's constraints */
+       /* Now scan the child's constraints to find matches */
        ScanKeyInit(&key[0],
                                Anum_pg_constraint_conrelid,
                                BTEqualStrategyNumber, F_OIDEQ,
@@ -16453,20 +16785,41 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached)
        while (HeapTupleIsValid(constraintTuple = systable_getnext(scan)))
        {
                Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTuple);
-               bool            match;
-
-               if (con->contype != CONSTRAINT_CHECK)
-                       continue;
+               bool            match = false;
 
-               match = false;
-               foreach_ptr(char, chkname, 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), chkname) == 0)
+                       foreach_ptr(char, chkname, connames)
                        {
-                               match = true;
-                               break;
+                               if (con->contype == CONSTRAINT_CHECK &&
+                                       strcmp(NameStr(con->conname), chkname) == 0)
+                               {
+                                       match = true;
+                                       connames = foreach_delete_current(connames, chkname);
+                                       break;
+                               }
+                       }
+               }
+               else if (con->contype == CONSTRAINT_NOTNULL)
+               {
+                       AttrNumber      child_attno = extractNotNullColumn(constraintTuple);
+
+                       foreach_int(prevattno, nncolumns)
+                       {
+                               if (prevattno == child_attno)
+                               {
+                                       match = true;
+                                       nncolumns = foreach_delete_current(nncolumns, prevattno);
+                                       break;
+                               }
                        }
                }
+               else
+                       continue;
 
                if (match)
                {
@@ -16487,6 +16840,12 @@ RemoveInheritance(Relation child_rel, Relation parent_rel, bool expect_detached)
                }
        }
 
+       /* We should have matched all constraints */
+       if (connames != NIL || nncolumns != NIL)
+               elog(ERROR, "%d unmatched constraints while removing inheritance from \"%s\" to \"%s\"",
+                        list_length(connames) + list_length(nncolumns),
+                        RelationGetRelationName(child_rel), RelationGetRelationName(parent_rel));
+
        systable_endscan(scan);
        table_close(catalogRelation, RowExclusiveLock);
 
@@ -19039,7 +19398,8 @@ AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel)
 
                /*
                 * If no suitable index was found in the partition-to-be, create one
-                * now.
+                * now.  Note that if this is a PK, not-null constraints must already
+                * exist.
                 */
                if (!found)
                {
@@ -19737,7 +20097,7 @@ ATExecDetachPartitionFinalize(Relation rel, RangeVar *name)
  * DetachAddConstraintIfNeeded
  *             Subroutine for ATExecDetachPartition.  Create a constraint that
  *             takes the place of the partition constraint, but avoid creating
- *             a dupe if an constraint already exists which implies the needed
+ *             a dupe if a constraint already exists which implies the needed
  *             constraint.
  */
 static void
@@ -19770,8 +20130,8 @@ DetachAddConstraintIfNeeded(List **wqueue, Relation partRel)
                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);
        }
 }
 
@@ -20040,6 +20400,13 @@ ATExecAttachPartitionIdx(List **wqueue, Relation parentIdx, RangeVar *name)
                                                                   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))
@@ -20183,6 +20550,29 @@ validatePartitionedIndex(Relation partedIdx, Relation partedTbl)
        }
 }
 
+/*
+ * 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.
index 2a6550de907b5b3ea6525036d3e4855a26751d01..859e2191f08f35a572959225b0c398127bf451b0 100644 (file)
@@ -944,6 +944,10 @@ DefineDomain(CreateDomainStmt *stmt)
                                        ereport(ERROR,
                                                        (errcode(ERRCODE_SYNTAX_ERROR),
                                                         errmsg("conflicting NULL/NOT NULL constraints")));
+                               if (constr->is_no_inherit)
+                                       ereport(ERROR,
+                                                       errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+                                                       errmsg("not-null constraints for domains cannot be marked NO INHERIT"));
                                typNotNull = true;
                                nullDefined = true;
                                break;
index 9cac3c1c27b82df78594e6e11ed5d6d396ebf8f8..7e5df7bea4dab5769e2b73e3a17727d31b08dc77 100644 (file)
@@ -436,6 +436,29 @@ makeRangeVar(char *schemaname, char *relname, int location)
        return r;
 }
 
+/*
+ * makeNotNullConstraint -
+ *             creates a Constraint node for NOT NULL constraints
+ */
+Constraint *
+makeNotNullConstraint(String *colname)
+{
+       Constraint *notnull;
+
+       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(colname);
+       notnull->skip_validation = false;
+       notnull->initially_valid = true;
+
+       return notnull;
+}
+
 /*
  * makeTypeName -
  *     build a TypeName node for an unqualified name.
index b913f91ff03b89bb2bf7a07cd982ba5a55ccb9dd..37b0ca2e4396595b1170d70a7bf6a8999f74996a 100644 (file)
@@ -1698,6 +1698,8 @@ relation_excluded_by_constraints(PlannerInfo *root,
         * 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);
 
index 89fdb94c237c038b4b5fc3ac808522e02b837233..67eb96396af927bf77d2ed6ecc977fca625be713 100644 (file)
@@ -3908,12 +3908,15 @@ ColConstraint:
  * 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
@@ -4150,6 +4153,20 @@ ConstraintElem:
                                        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_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
                                ConstraintAttributeSpec
                                {
@@ -4317,10 +4334,10 @@ DomainConstraintElem:
                                        n->contype = CONSTR_NOTNULL;
                                        n->location = @1;
                                        n->keys = list_make1(makeString("value"));
-                                       /* no NOT VALID support yet */
+                                       /* no NOT VALID, NO INHERIT support */
                                        processCASbits($3, @3, "NOT NULL",
                                                                   NULL, NULL, NULL,
-                                                                  &n->is_no_inherit, yyscanner);
+                                                                  NULL, yyscanner);
                                        n->initially_valid = true;
                                        $$ = (Node *) n;
                                }
index 1e15ce10b48e4f4a69f8c8f381624a9a5b9da836..0f324ee4e319ee72b88f68fe4541b3e1ed0f5d78 100644 (file)
@@ -81,6 +81,7 @@ typedef struct
        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 */
@@ -240,6 +241,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
        cxt.isalter = false;
        cxt.columns = NIL;
        cxt.ckconstraints = NIL;
+       cxt.nnconstraints = NIL;
        cxt.fkconstraints = NIL;
        cxt.ixconstraints = NIL;
        cxt.likeclauses = NIL;
@@ -303,6 +305,32 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 
        Assert(stmt->constraints == NIL);
 
+       /*
+        * Before processing index constraints, which could include a primary key,
+        * we must scan all not-null constraints to propagate the is_not_null flag
+        * to each corresponding ColumnDef.  This is necessary because table-level
+        * not-null constraints have not been marked in each ColumnDef, and the PK
+        * processing code needs to know whether one constraint has already been
+        * declared in order not to declare a redundant one.
+        */
+       foreach_node(Constraint, nn, cxt.nnconstraints)
+       {
+               char       *colname = strVal(linitial(nn->keys));
+
+               foreach_node(ColumnDef, cd, cxt.columns)
+               {
+                       /* not our column? */
+                       if (strcmp(cd->colname, colname) != 0)
+                               continue;
+                       /* Already marked not-null? Nothing to do */
+                       if (cd->is_not_null)
+                               break;
+                       /* Bingo, we're done for this constraint */
+                       cd->is_not_null = true;
+                       break;
+               }
+       }
+
        /*
         * Postprocess constraints that give rise to index definitions.
         */
@@ -340,6 +368,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
         */
        stmt->tableElts = cxt.columns;
        stmt->constraints = cxt.ckconstraints;
+       stmt->nnconstraints = cxt.nnconstraints;
 
        result = lappend(cxt.blist, stmt);
        result = list_concat(result, cxt.alist);
@@ -566,7 +595,9 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
        bool            saw_default;
        bool            saw_identity;
        bool            saw_generated;
-       ListCell   *clist;
+       bool            need_notnull = false;
+       bool            disallow_noinherit_notnull = false;
+       Constraint *notnull_constraint = NULL;
 
        cxt->columns = lappend(cxt->columns, column);
 
@@ -663,28 +694,54 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *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;
+               disallow_noinherit_notnull = true;
        }
 
        /* Process column constraints, if any... */
        transformConstraintAttrs(cxt, column->constraints);
 
+       /*
+        * First, scan the column's constraints to see if a not-null constraint
+        * that we add must be prevented from being NO INHERIT.  This should be
+        * enforced only for PRIMARY KEY, not IDENTITY or SERIAL.  However, if the
+        * not-null constraint is specified as a table constraint rather than as a
+        * column constraint, AddRelationNotNullConstraints would raise an error
+        * if a NO INHERIT mismatch is found.  To avoid inconsistently disallowing
+        * it in the table constraint case but not the column constraint case, we
+        * disallow it here as well.  Maybe AddRelationNotNullConstraints can be
+        * improved someday, so that it doesn't complain, and then we can remove
+        * the restriction for SERIAL and IDENTITY here as well.
+        */
+       if (!disallow_noinherit_notnull)
+       {
+               foreach_node(Constraint, constraint, column->constraints)
+               {
+                       switch (constraint->contype)
+                       {
+                               case CONSTR_IDENTITY:
+                               case CONSTR_PRIMARY:
+                                       disallow_noinherit_notnull = true;
+                                       break;
+                               default:
+                                       break;
+                       }
+               }
+       }
+
+       /* Now scan them again to do full processing */
        saw_nullable = false;
        saw_default = false;
        saw_identity = false;
        saw_generated = false;
 
-       foreach(clist, column->constraints)
+       foreach_node(Constraint, constraint, column->constraints)
        {
-               Constraint *constraint = lfirst_node(Constraint, clist);
-
                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\"",
@@ -696,6 +753,12 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
                                break;
 
                        case CONSTR_NOTNULL:
+                               if (cxt->ispartitioned && constraint->is_no_inherit)
+                                       ereport(ERROR,
+                                                       errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                                       errmsg("not-null constraints on partitioned tables cannot be NO INHERIT"));
+
+                               /* Disallow conflicting [NOT] NULL markings */
                                if (saw_nullable && !column->is_not_null)
                                        ereport(ERROR,
                                                        (errcode(ERRCODE_SYNTAX_ERROR),
@@ -703,8 +766,52 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
                                                                        column->colname, cxt->relation->relname),
                                                         parser_errposition(cxt->pstate,
                                                                                                constraint->location)));
-                               column->is_not_null = true;
-                               saw_nullable = true;
+
+                               if (disallow_noinherit_notnull && constraint->is_no_inherit)
+                                       ereport(ERROR,
+                                                       errcode(ERRCODE_SYNTAX_ERROR),
+                                                       errmsg("conflicting NO INHERIT declarations for not-null constraints on column \"%s\"",
+                                                                  column->colname));
+
+                               /*
+                                * If this is the first time we see this column being marked
+                                * not-null, add the constraint entry and keep track of it.
+                                * Also, remove previous markings that we need one.
+                                *
+                                * If this is a redundant not-null specification, just check
+                                * that it doesn't conflict with what was specified earlier.
+                                *
+                                * Any conflicts with table constraints will be further
+                                * checked in AddRelationNotNullConstraints().
+                                */
+                               if (!column->is_not_null)
+                               {
+                                       column->is_not_null = true;
+                                       saw_nullable = true;
+                                       need_notnull = false;
+
+                                       constraint->keys = list_make1(makeString(column->colname));
+                                       notnull_constraint = constraint;
+                                       cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
+                               }
+                               else if (notnull_constraint)
+                               {
+                                       if (constraint->conname &&
+                                               notnull_constraint->conname &&
+                                               strcmp(notnull_constraint->conname, constraint->conname) != 0)
+                                               elog(ERROR, "conflicting not-null constraint names \"%s\" and \"%s\"",
+                                                        notnull_constraint->conname, constraint->conname);
+
+                                       if (notnull_constraint->is_no_inherit != constraint->is_no_inherit)
+                                               ereport(ERROR,
+                                                               errcode(ERRCODE_SYNTAX_ERROR),
+                                                               errmsg("conflicting NO INHERIT declarations for not-null constraints on column \"%s\"",
+                                                                          column->colname));
+
+                                       if (!notnull_constraint->conname && constraint->conname)
+                                               notnull_constraint->conname = constraint->conname;
+                               }
+
                                break;
 
                        case CONSTR_DEFAULT:
@@ -754,16 +861,19 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
                                        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;
                                }
 
@@ -790,6 +900,15 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
                                break;
 
                        case CONSTR_PRIMARY:
+                               if (saw_nullable && !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)));
+                               need_notnull = true;
+
                                if (cxt->isforeign)
                                        ereport(ERROR,
                                                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
@@ -869,6 +988,17 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column)
                                                                                constraint->location)));
        }
 
+       /*
+        * If we need a not-null constraint for PRIMARY KEY, SERIAL or IDENTITY,
+        * and one was not explicitly specified, add one now.
+        */
+       if (need_notnull && !(saw_nullable && column->is_not_null))
+       {
+               column->is_not_null = true;
+               notnull_constraint = makeNotNullConstraint(makeString(column->colname));
+               cxt->nnconstraints = lappend(cxt->nnconstraints, notnull_constraint);
+       }
+
        /*
         * If needed, generate ALTER&nbs