ALTER DOMAIN ADD NOT NULL NOT VALID

Started by jian he8 months ago9 messages
#1jian he
jian.universality@gmail.com
1 attachment(s)

hi.

attached patch is for $subject implementation

per https://www.postgresql.org/docs/current/sql-alterdomain.html
"""
Although ALTER DOMAIN ADD CONSTRAINT attempts to verify that existing stored
data satisfies the new constraint, this check is not bulletproof, because the
command cannot “see” table rows that are newly inserted or updated and not yet
committed. If there is a hazard that concurrent operations might insert bad
data, the way to proceed is to add the constraint using the NOT VALID option,
commit that command, wait until all transactions started before that commit have
finished, and then issue ALTER DOMAIN VALIDATE CONSTRAINT to search for data
violating the constraint.
"""

Obviously, the above behavior can also happen to not-null constraints.
add NOT NULL NOT VALID is good for validation invalid data too.

the not valid information is displayed at column "Nullable"
for example:

\dD things
List of domains
Schema | Name | Type | Collation | Nullable | Default
| Check
--------+--------+---------+-----------+--------------------+---------+--------------------
public | things | integer | | not null not valid |
| CHECK (VALUE < 11)

Attachments:

v1-0001-ALTER-DOMAIN-ADD-NOT-NULL-NOT-VALID.patchtext/x-patch; charset=US-ASCII; name=v1-0001-ALTER-DOMAIN-ADD-NOT-NULL-NOT-VALID.patchDownload
From bdea7a60eab6e46d7f0b59b9a5602d6a2421f60d Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 21 May 2025 18:43:17 +0800
Subject: [PATCH v1 1/1] ALTER DOMAIN ADD NOT NULL NOT VALID

we have NOT NULL NO VALID for table constraints, now we can make domain have NOT
VALID not-null constraint too.

ALTER DOMAIN SET NOT NULL works similarly to its ALTER TABLE counterpart.  It
validates an existing invalid NOT NULL constraint and updates
pg_constraint.convalidated to true.

ALTER DOMAIN ADD NOT NULL will add a new, valid not-null constraint.  If the
domain already has an NOT VALID not-null constraint, the command will result in
an error.
If domain already have VALID not-null, add a NOT VALID is no-op.

\dD changes:
for domain's not null not valid constraint: now column "Nullable" will display as
"not null not valid".
domain valid not-null constraint works as before.

discussed: https://postgr.es/m/
---
 doc/src/sgml/catalogs.sgml           |   1 +
 doc/src/sgml/ref/alter_domain.sgml   |   1 -
 src/backend/catalog/pg_constraint.c  |  10 +--
 src/backend/commands/typecmds.c      | 115 +++++++++++++++++++++++++--
 src/backend/parser/gram.y            |   8 +-
 src/bin/pg_dump/pg_dump.c            |  27 +++++--
 src/bin/pg_dump/t/002_pg_dump.pl     |  16 ++++
 src/bin/psql/describe.c              |   4 +-
 src/test/regress/expected/domain.out |  40 ++++++++++
 src/test/regress/sql/domain.sql      |  21 +++++
 10 files changed, 216 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index cbd4e40a320..fd1b4b0a563 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9515,6 +9515,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        <structfield>typnotnull</structfield> represents a not-null
        constraint on a type.  Used for domains only.
+       Note: the not-null constraint on domain may not validated.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/alter_domain.sgml b/doc/src/sgml/ref/alter_domain.sgml
index 74855172222..4807116eb05 100644
--- a/doc/src/sgml/ref/alter_domain.sgml
+++ b/doc/src/sgml/ref/alter_domain.sgml
@@ -92,7 +92,6 @@ ALTER DOMAIN <replaceable class="parameter">name</replaceable>
       valid using <command>ALTER DOMAIN ... VALIDATE CONSTRAINT</command>.
       Newly inserted or updated rows are always checked against all
       constraints, even those marked <literal>NOT VALID</literal>.
-      <literal>NOT VALID</literal> is only accepted for <literal>CHECK</literal> constraints.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 2d5ac1ea813..27d2fee52d3 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -651,8 +651,8 @@ findNotNullConstraint(Oid relid, const char *colname)
 }
 
 /*
- * Find and return the pg_constraint tuple that implements a validated
- * not-null constraint for the given domain.
+ * Find and return the pg_constraint tuple that implements not-null constraint
+ * for the given domain. it may be NOT VALID.
  */
 HeapTuple
 findDomainNotNullConstraint(Oid typid)
@@ -675,13 +675,9 @@ findDomainNotNullConstraint(Oid typid)
 	{
 		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
 
-		/*
-		 * We're looking for a NOTNULL constraint that's marked validated.
-		 */
+		/* We're looking for a NOTNULL constraint */
 		if (con->contype != CONSTRAINT_NOTNULL)
 			continue;
-		if (!con->convalidated)
-			continue;
 
 		/* Found it */
 		retval = heap_copytuple(conTup);
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 45ae7472ab5..15549673f1e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -2757,12 +2757,70 @@ AlterDomainNotNull(List *names, bool notNull)
 	checkDomainOwner(tup);
 
 	/* Is the domain already set to the desired constraint? */
-	if (typTup->typnotnull == notNull)
+	if (typTup->typnotnull == notNull && !notNull)
 	{
 		table_close(typrel, RowExclusiveLock);
 		return address;
 	}
 
+	if (typTup->typnotnull == notNull && notNull)
+	{
+		ScanKeyData key[1];
+		SysScanDesc scan;
+		Relation	pg_constraint;
+		Form_pg_constraint copy_con;
+		HeapTuple	conTup;
+		HeapTuple	copyTuple;
+
+		pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+
+		/* Look for NOT NULL Constraints on this domain */
+		ScanKeyInit(&key[0],
+					Anum_pg_constraint_contypid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(domainoid));
+
+		scan = systable_beginscan(pg_constraint, ConstraintTypidIndexId, true,
+								  NULL, 1, key);
+
+		while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+		{
+			Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+			if (con->contype != CONSTRAINT_NOTNULL)
+				continue;
+
+			/*
+			 * ALTER DOMAIN SET NOT NULL will validate the NOT VALID not-null
+			 * constraint, also set the pg_constraint row convalidated to true.
+			*/
+			if (!con->convalidated)
+			{
+				validateDomainNotNullConstraint(domainoid);
+				copyTuple = heap_copytuple(conTup);
+
+				copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+				copy_con->convalidated = true;
+				CatalogTupleUpdate(pg_constraint, &copyTuple->t_self, copyTuple);
+
+				InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+				heap_freetuple(copyTuple);
+			}
+			break;
+		}
+		systable_endscan(scan);
+		table_close(pg_constraint, AccessShareLock);
+
+		table_close(typrel, RowExclusiveLock);
+
+		/*
+		 * If we already validated the existing NOT VALID not-null constraint
+		 * then we don't need install another not-null constraint, exit now.
+		*/
+		return address;
+	}
+
 	if (notNull)
 	{
 		Constraint *constr;
@@ -2990,7 +3048,44 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
 	}
 	else if (constr->contype == CONSTR_NOTNULL)
 	{
-		/* Is the domain already set NOT NULL? */
+		HeapTuple	conTup;
+		ScanKeyData key[1];
+		SysScanDesc scan;
+		Relation	pg_constraint;
+
+		pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+
+		/* Look for NOT NULL Constraints on this domain */
+		ScanKeyInit(&key[0],
+					Anum_pg_constraint_contypid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(domainoid));
+
+		scan = systable_beginscan(pg_constraint, ConstraintTypidIndexId, true,
+								  NULL, 1, key);
+
+		while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+		{
+			Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+			if (con->contype != CONSTRAINT_NOTNULL)
+				continue;
+
+			/*
+			 * can not add valid not-null if domain already have NOT VALID NOT NULL.
+			*/
+			if (!constr->skip_validation && !con->convalidated)
+				ereport(ERROR,
+						errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+						errmsg("incompatible NOT VALID constraint \"%s\" on domain \"%s\"",
+								NameStr(con->conname), NameStr(typTup->typname)),
+						errhint("You might need to validate it using %s.",
+								"ALTER DOMAIN ... VALIDATE CONSTRAINT"));
+			break;
+		}
+		systable_endscan(scan);
+		table_close(pg_constraint, AccessShareLock);
+
 		if (typTup->typnotnull)
 		{
 			table_close(typrel, RowExclusiveLock);
@@ -3081,16 +3176,20 @@ AlterDomainValidateConstraint(List *names, const char *constrName)
 						constrName, TypeNameToString(typename))));
 
 	con = (Form_pg_constraint) GETSTRUCT(tuple);
-	if (con->contype != CONSTRAINT_CHECK)
+	if (con->contype != CONSTRAINT_CHECK && con->contype != CONSTRAINT_NOTNULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("constraint \"%s\" of domain \"%s\" is not a check constraint",
+				 errmsg("constraint \"%s\" of domain \"%s\" is not a check or not-null constraint",
 						constrName, TypeNameToString(typename))));
 
-	val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin);
-	conbin = TextDatumGetCString(val);
-
-	validateDomainCheckConstraint(domainoid, conbin);
+	if (con->contype == CONSTRAINT_CHECK)
+	{
+		val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin);
+		conbin = TextDatumGetCString(val);
+		validateDomainCheckConstraint(domainoid, conbin);
+	}
+	else
+		validateDomainNotNullConstraint(domainoid);
 
 	/*
 	 * Now update the catalog, while we have the door open.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0b5652071d1..8625ff82147 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4391,11 +4391,13 @@ DomainConstraintElem:
 					n->contype = CONSTR_NOTNULL;
 					n->location = @1;
 					n->keys = list_make1(makeString("value"));
-					/* no NOT VALID, NO INHERIT support */
+					/* NO INHERIT is not supported */
 					processCASbits($3, @3, "NOT NULL",
 								   NULL, NULL, NULL,
-								   NULL, NULL, yyscanner);
-					n->initially_valid = true;
+								   &n->skip_validation,
+								   NULL, yyscanner);
+					n->is_enforced = true;
+					n->initially_valid = !n->skip_validation;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c73e73a87d1..4ebc96f8564 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8250,7 +8250,8 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
 	int			i_tableoid,
 				i_oid,
 				i_conname,
-				i_consrc;
+				i_consrc,
+				i_contype;
 	int			ntups;
 
 	if (!fout->is_prepared[PREPQUERY_GETDOMAINCONSTRAINTS])
@@ -8260,9 +8261,10 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
 							 "PREPARE getDomainConstraints(pg_catalog.oid) AS\n"
 							 "SELECT tableoid, oid, conname, "
 							 "pg_catalog.pg_get_constraintdef(oid) AS consrc, "
-							 "convalidated "
+							 "convalidated, "
+							 "contype "
 							 "FROM pg_catalog.pg_constraint "
-							 "WHERE contypid = $1 AND contype = 'c' "
+							 "WHERE contypid = $1 "
 							 "ORDER BY conname");
 
 		ExecuteSqlStatement(fout, query->data);
@@ -8282,6 +8284,7 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
 	i_oid = PQfnumber(res, "oid");
 	i_conname = PQfnumber(res, "conname");
 	i_consrc = PQfnumber(res, "consrc");
+	i_contype = PQfnumber(res, "contype");
 
 	constrinfo = (ConstraintInfo *) pg_malloc(ntups * sizeof(ConstraintInfo));
 
@@ -8300,7 +8303,7 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
 		constrinfo[i].dobj.namespace = tyinfo->dobj.namespace;
 		constrinfo[i].contable = NULL;
 		constrinfo[i].condomain = tyinfo;
-		constrinfo[i].contype = 'c';
+		constrinfo[i].contype = *(PQgetvalue(res, i, i_contype));
 		constrinfo[i].condef = pg_strdup(PQgetvalue(res, i, i_consrc));
 		constrinfo[i].confrelid = InvalidOid;
 		constrinfo[i].conindex = 0;
@@ -12484,8 +12487,18 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo)
 			appendPQExpBuffer(q, " COLLATE %s", fmtQualifiedDumpable(coll));
 	}
 
-	if (typnotnull[0] == 't')
+	if (typnotnull[0] == 't' && fout->remoteVersion < 190000)
 		appendPQExpBufferStr(q, " NOT NULL");
+	else
+	{
+		for (i = 0; i < tyinfo->nDomChecks; i++)
+		{
+			ConstraintInfo *domcheck = &(tyinfo->domChecks[i]);
+
+			if (!domcheck->separate && domcheck->contype == 'n')
+				appendPQExpBufferStr(q, " NOT NULL");
+		}
+	}
 
 	if (typdefault != NULL)
 	{
@@ -12505,7 +12518,7 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo)
 	{
 		ConstraintInfo *domcheck = &(tyinfo->domChecks[i]);
 
-		if (!domcheck->separate)
+		if (!domcheck->separate && domcheck->contype == 'c')
 			appendPQExpBuffer(q, "\n\tCONSTRAINT %s %s",
 							  fmtId(domcheck->dobj.name), domcheck->condef);
 	}
@@ -18375,7 +18388,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 										  .dropStmt = delq->data));
 		}
 	}
-	else if (coninfo->contype == 'c' && tbinfo == NULL)
+	else if (tbinfo == NULL)
 	{
 		/* CHECK constraint on a domain */
 		TypeInfo   *tyinfo = coninfo->condomain;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index cf34f71ea11..f15d22395c6 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1146,6 +1146,22 @@ my %tests = (
 		},
 	},
 
+	'DOMAIN CONSTRAINT NOT NULL / NOT VALID' => {
+		create_sql => 'CREATE DOMAIN dump_test.test_domain_nn AS INT;
+					   ALTER DOMAIN dump_test.test_domain_nn ADD CONSTRAINT nn NOT NULL NOT VALID;',
+		regexp => qr/^
+			\QALTER DOMAIN dump_test.test_domain_nn\E \n^\s+
+			\QADD CONSTRAINT nn NOT NULL NOT VALID;\E
+			/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_post_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	'CONSTRAINT NOT NULL / NOT VALID (child1)' => {
 		regexp => qr/^
 		\QCREATE TABLE dump_test.test_table_nn_chld1 (\E\n
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1d08268393e..f7c6d56fc01 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -4558,7 +4558,9 @@ listDomains(const char *pattern, bool verbose, bool showSystem)
 					  "       pg_catalog.format_type(t.typbasetype, t.typtypmod) as \"%s\",\n"
 					  "       (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
 					  "        WHERE c.oid = t.typcollation AND bt.oid = t.typbasetype AND t.typcollation <> bt.typcollation) as \"%s\",\n"
-					  "       CASE WHEN t.typnotnull THEN 'not null' END as \"%s\",\n"
+					  "       CASE WHEN t.typnotnull THEN "
+					  "       (SELECT lower(pg_catalog.pg_get_constraintdef(r.oid, true)) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid AND r.contype = " CppAsString2(CONSTRAINT_NOTNULL) ")"
+					  "       END as \"%s\",\n"
 					  "       t.typdefault as \"%s\",\n"
 					  "       pg_catalog.array_to_string(ARRAY(\n"
 					  "         SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid AND r.contype = " CppAsString2(CONSTRAINT_CHECK) " ORDER BY r.conname\n"
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index ba6f05eeb7d..c4863ba9b53 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -926,6 +926,46 @@ ALTER DOMAIN things VALIDATE CONSTRAINT meow;
 ERROR:  column "stuff" of table "thethings" contains values that violate the new constraint
 UPDATE thethings SET stuff = 10;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
+SELECT * FROM thethings ORDER BY 1;
+ stuff 
+-------
+    10
+(1 row)
+
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL;
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --no-op
+ALTER DOMAIN things DROP NOT NULL;
+INSERT INTO thethings VALUES(NULL);
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --ok
+INSERT INTO thethings VALUES(NULL); --error
+ERROR:  domain things does not allow null values
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL; --error
+ERROR:  incompatible NOT VALID constraint "domain_nn" on domain "things"
+HINT:  You might need to validate it using ALTER DOMAIN ... VALIDATE CONSTRAINT.
+ALTER DOMAIN things SET NOT NULL; --error
+ERROR:  column "stuff" of table "thethings" contains null values
+ALTER DOMAIN things ADD CONSTRAINT domain_nn1 NOT NULL NOT VALID; --no-op
+\dD things
+                                      List of domains
+ Schema |  Name  |  Type   | Collation |      Nullable      | Default |       Check        
+--------+--------+---------+-----------+--------------------+---------+--------------------
+ public | things | integer |           | not null not valid |         | CHECK (VALUE < 11)
+(1 row)
+
+SELECT conname, pg_get_constraintdef(oid)
+FROM pg_constraint
+WHERE contypid = 'things'::regtype;
+  conname  | pg_get_constraintdef 
+-----------+----------------------
+ meow      | CHECK ((VALUE < 11))
+ domain_nn | NOT NULL NOT VALID
+(2 rows)
+
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --error
+ERROR:  column "stuff" of table "thethings" contains null values
+UPDATE thethings SET stuff = 10 WHERE stuff IS NULL;
+ALTER DOMAIN things SET NOT NULL; --ok
+ALTER DOMAIN things DROP NOT NULL; --ok
 -- Confirm ALTER DOMAIN with RULES.
 create table domtab (col1 integer);
 create domain dom as integer;
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b752a63ab5f..9b5a76af48b 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -536,6 +536,27 @@ ALTER DOMAIN things ADD CONSTRAINT meow CHECK (VALUE < 11) NOT VALID;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
 UPDATE thethings SET stuff = 10;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
+SELECT * FROM thethings ORDER BY 1;
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL;
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --no-op
+ALTER DOMAIN things DROP NOT NULL;
+
+INSERT INTO thethings VALUES(NULL);
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --ok
+INSERT INTO thethings VALUES(NULL); --error
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL; --error
+ALTER DOMAIN things SET NOT NULL; --error
+ALTER DOMAIN things ADD CONSTRAINT domain_nn1 NOT NULL NOT VALID; --no-op
+
+\dD things
+SELECT conname, pg_get_constraintdef(oid)
+FROM pg_constraint
+WHERE contypid = 'things'::regtype;
+
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --error
+UPDATE thethings SET stuff = 10 WHERE stuff IS NULL;
+ALTER DOMAIN things SET NOT NULL; --ok
+ALTER DOMAIN things DROP NOT NULL; --ok
 
 -- Confirm ALTER DOMAIN with RULES.
 create table domtab (col1 integer);
-- 
2.34.1

#2Quan Zongliang
quanzongliang@yeah.net
In reply to: jian he (#1)
Re: ALTER DOMAIN ADD NOT NULL NOT VALID

On 2025/5/21 18:44, jian he wrote:

hi.

attached patch is for $subject implementation

per https://www.postgresql.org/docs/current/sql-alterdomain.html
"""
Although ALTER DOMAIN ADD CONSTRAINT attempts to verify that existing stored
data satisfies the new constraint, this check is not bulletproof, because the
command cannot “see” table rows that are newly inserted or updated and not yet
committed. If there is a hazard that concurrent operations might insert bad
data, the way to proceed is to add the constraint using the NOT VALID option,
commit that command, wait until all transactions started before that commit have
finished, and then issue ALTER DOMAIN VALIDATE CONSTRAINT to search for data
violating the constraint.
"""

Obviously, the above behavior can also happen to not-null constraints.
add NOT NULL NOT VALID is good for validation invalid data too.

the not valid information is displayed at column "Nullable"
for example:

\dD things
List of domains
Schema | Name | Type | Collation | Nullable | Default
| Check
--------+--------+---------+-----------+--------------------+---------+--------------------
public | things | integer | | not null not valid |
| CHECK (VALUE < 11)

It makes sense to support the "NOT NULL NOT VALID" option.

The two if statements in the AlterDomainNotNull() should be adjusted.

if (typTup->typnotnull == notNull && !notNull)
==>
if (!notNull && !typTup->typnotnull)

if (typTup->typnotnull == notNull && notNull)
==>
if (notNull && typTup->typnotnull)

--
Quan Zongliang

#3jian he
jian.universality@gmail.com
In reply to: Quan Zongliang (#2)
1 attachment(s)
Re: ALTER DOMAIN ADD NOT NULL NOT VALID

On Thu, May 22, 2025 at 10:08 PM Quan Zongliang <quanzongliang@yeah.net> wrote:

It makes sense to support the "NOT NULL NOT VALID" option.

The two if statements in the AlterDomainNotNull() should be adjusted.

if (typTup->typnotnull == notNull && !notNull)
==>
if (!notNull && !typTup->typnotnull)

if (typTup->typnotnull == notNull && notNull)
==>
if (notNull && typTup->typnotnull)

yech, you are right.

There is one failed test [0]https://api.cirrus-ci.com/v1/artifact/task/6676676240736256/testrun/build/testrun/pg_upgrade/002_pg_upgrade/data/regression.diffs because of query result order,
so I add "ORDER BY conname COLLATE "C";" to stabilize tests.

[0]: https://api.cirrus-ci.com/v1/artifact/task/6676676240736256/testrun/build/testrun/pg_upgrade/002_pg_upgrade/data/regression.diffs

Attachments:

v2-0001-ALTER-DOMAIN-ADD-NOT-NULL-NOT-VALID.patchtext/x-patch; charset=US-ASCII; name=v2-0001-ALTER-DOMAIN-ADD-NOT-NULL-NOT-VALID.patchDownload
From a840278f579b57cab4c13b91abfcc4f44a6d8a83 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 28 May 2025 15:32:03 +0800
Subject: [PATCH v2 1/1] ALTER DOMAIN ADD NOT NULL NOT VALID

we have NOT NULL NO VALID for table constraints, we can make domain have NOT
VALID not-null constraint too.

ALTER DOMAIN SET NOT NULL works similarly to its ALTER TABLE counterpart.  It
validates an existing invalid NOT NULL constraint and updates
pg_constraint.convalidated to true.

ALTER DOMAIN ADD NOT NULL will add a new, valid not-null constraint.  If the
domain already has an NOT VALID not-null constraint, the command will result in
an error.
If domain already have VALID not-null, add a NOT VALID is no-op.

\dD changes:
for domain's not null not valid constraint: now column "Nullable" will display as
"not null not valid".
domain valid not-null constraint works as before.

discussed: https://postgr.es/m/CACJufxGcABLgmH951SJkkihK+FW8KR3=odBhXEVCF9atQbur2Q@mail.gmail.com
---
 doc/src/sgml/catalogs.sgml           |   1 +
 doc/src/sgml/ref/alter_domain.sgml   |   1 -
 src/backend/catalog/pg_constraint.c  |  10 +--
 src/backend/commands/typecmds.c      | 116 +++++++++++++++++++++++++--
 src/backend/parser/gram.y            |   8 +-
 src/bin/pg_dump/pg_dump.c            |  27 +++++--
 src/bin/pg_dump/t/002_pg_dump.pl     |  16 ++++
 src/bin/psql/describe.c              |   4 +-
 src/test/regress/expected/domain.out |  41 ++++++++++
 src/test/regress/sql/domain.sql      |  22 +++++
 10 files changed, 219 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index cbd4e40a320..fd1b4b0a563 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9515,6 +9515,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        <structfield>typnotnull</structfield> represents a not-null
        constraint on a type.  Used for domains only.
+       Note: the not-null constraint on domain may not validated.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/alter_domain.sgml b/doc/src/sgml/ref/alter_domain.sgml
index 74855172222..4807116eb05 100644
--- a/doc/src/sgml/ref/alter_domain.sgml
+++ b/doc/src/sgml/ref/alter_domain.sgml
@@ -92,7 +92,6 @@ ALTER DOMAIN <replaceable class="parameter">name</replaceable>
       valid using <command>ALTER DOMAIN ... VALIDATE CONSTRAINT</command>.
       Newly inserted or updated rows are always checked against all
       constraints, even those marked <literal>NOT VALID</literal>.
-      <literal>NOT VALID</literal> is only accepted for <literal>CHECK</literal> constraints.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 2d5ac1ea813..27d2fee52d3 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -651,8 +651,8 @@ findNotNullConstraint(Oid relid, const char *colname)
 }
 
 /*
- * Find and return the pg_constraint tuple that implements a validated
- * not-null constraint for the given domain.
+ * Find and return the pg_constraint tuple that implements not-null constraint
+ * for the given domain. it may be NOT VALID.
  */
 HeapTuple
 findDomainNotNullConstraint(Oid typid)
@@ -675,13 +675,9 @@ findDomainNotNullConstraint(Oid typid)
 	{
 		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
 
-		/*
-		 * We're looking for a NOTNULL constraint that's marked validated.
-		 */
+		/* We're looking for a NOTNULL constraint */
 		if (con->contype != CONSTRAINT_NOTNULL)
 			continue;
-		if (!con->convalidated)
-			continue;
 
 		/* Found it */
 		retval = heap_copytuple(conTup);
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 45ae7472ab5..12744e906ec 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -2757,12 +2757,70 @@ AlterDomainNotNull(List *names, bool notNull)
 	checkDomainOwner(tup);
 
 	/* Is the domain already set to the desired constraint? */
-	if (typTup->typnotnull == notNull)
+	if (!typTup->typnotnull && !notNull)
 	{
 		table_close(typrel, RowExclusiveLock);
 		return address;
 	}
 
+	if (typTup->typnotnull && notNull)
+	{
+		ScanKeyData key[1];
+		SysScanDesc scan;
+		Relation	pg_constraint;
+		Form_pg_constraint copy_con;
+		HeapTuple	conTup;
+		HeapTuple	copyTuple;
+
+		pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+
+		/* Look for NOT NULL Constraints on this domain */
+		ScanKeyInit(&key[0],
+					Anum_pg_constraint_contypid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(domainoid));
+
+		scan = systable_beginscan(pg_constraint, ConstraintTypidIndexId, true,
+								  NULL, 1, key);
+
+		while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+		{
+			Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+			if (con->contype != CONSTRAINT_NOTNULL)
+				continue;
+
+			/*
+			 * ALTER DOMAIN SET NOT NULL will validate the NOT VALID not-null
+			 * constraint, also set the pg_constraint.convalidated to true.
+			*/
+			if (!con->convalidated)
+			{
+				validateDomainNotNullConstraint(domainoid);
+				copyTuple = heap_copytuple(conTup);
+
+				copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+				copy_con->convalidated = true;
+				CatalogTupleUpdate(pg_constraint, &copyTuple->t_self, copyTuple);
+
+				InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+				heap_freetuple(copyTuple);
+			}
+			break;
+		}
+		systable_endscan(scan);
+		table_close(pg_constraint, AccessShareLock);
+
+		table_close(typrel, RowExclusiveLock);
+
+		/*
+		 * If we already validated the existing NOT VALID not-null constraint
+		 * then we don't need install another not-null constraint, exit now.
+		*/
+		return address;
+	}
+
 	if (notNull)
 	{
 		Constraint *constr;
@@ -2990,7 +3048,45 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
 	}
 	else if (constr->contype == CONSTR_NOTNULL)
 	{
-		/* Is the domain already set NOT NULL? */
+		HeapTuple	conTup;
+		ScanKeyData key[1];
+		SysScanDesc scan;
+		Relation	pg_constraint;
+
+		pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+
+		/* Look for NOT NULL Constraints on this domain */
+		ScanKeyInit(&key[0],
+					Anum_pg_constraint_contypid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(domainoid));
+
+		scan = systable_beginscan(pg_constraint, ConstraintTypidIndexId, true,
+								  NULL, 1, key);
+
+		while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+		{
+			Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+			if (con->contype != CONSTRAINT_NOTNULL)
+				continue;
+
+			/*
+			 * can not add not-null constraint if the domain already have NOT
+			 * VALID not-null constraint
+			*/
+			if (!constr->skip_validation && !con->convalidated)
+				ereport(ERROR,
+						errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+						errmsg("incompatible NOT VALID constraint \"%s\" on domain \"%s\"",
+								NameStr(con->conname), NameStr(typTup->typname)),
+						errhint("You might need to validate it using %s.",
+								"ALTER DOMAIN ... VALIDATE CONSTRAINT"));
+			break;
+		}
+		systable_endscan(scan);
+		table_close(pg_constraint, AccessShareLock);
+
 		if (typTup->typnotnull)
 		{
 			table_close(typrel, RowExclusiveLock);
@@ -3081,16 +3177,20 @@ AlterDomainValidateConstraint(List *names, const char *constrName)
 						constrName, TypeNameToString(typename))));
 
 	con = (Form_pg_constraint) GETSTRUCT(tuple);
-	if (con->contype != CONSTRAINT_CHECK)
+	if (con->contype != CONSTRAINT_CHECK && con->contype != CONSTRAINT_NOTNULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("constraint \"%s\" of domain \"%s\" is not a check constraint",
+				 errmsg("constraint \"%s\" of domain \"%s\" is not a check or not-null constraint",
 						constrName, TypeNameToString(typename))));
 
-	val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin);
-	conbin = TextDatumGetCString(val);
-
-	validateDomainCheckConstraint(domainoid, conbin);
+	if (con->contype == CONSTRAINT_CHECK)
+	{
+		val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin);
+		conbin = TextDatumGetCString(val);
+		validateDomainCheckConstraint(domainoid, conbin);
+	}
+	else
+		validateDomainNotNullConstraint(domainoid);
 
 	/*
 	 * Now update the catalog, while we have the door open.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0b5652071d1..8625ff82147 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4391,11 +4391,13 @@ DomainConstraintElem:
 					n->contype = CONSTR_NOTNULL;
 					n->location = @1;
 					n->keys = list_make1(makeString("value"));
-					/* no NOT VALID, NO INHERIT support */
+					/* NO INHERIT is not supported */
 					processCASbits($3, @3, "NOT NULL",
 								   NULL, NULL, NULL,
-								   NULL, NULL, yyscanner);
-					n->initially_valid = true;
+								   &n->skip_validation,
+								   NULL, yyscanner);
+					n->is_enforced = true;
+					n->initially_valid = !n->skip_validation;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 37432e66efd..88e825f24fe 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8250,7 +8250,8 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
 	int			i_tableoid,
 				i_oid,
 				i_conname,
-				i_consrc;
+				i_consrc,
+				i_contype;
 	int			ntups;
 
 	if (!fout->is_prepared[PREPQUERY_GETDOMAINCONSTRAINTS])
@@ -8260,9 +8261,10 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
 							 "PREPARE getDomainConstraints(pg_catalog.oid) AS\n"
 							 "SELECT tableoid, oid, conname, "
 							 "pg_catalog.pg_get_constraintdef(oid) AS consrc, "
-							 "convalidated "
+							 "convalidated, "
+							 "contype "
 							 "FROM pg_catalog.pg_constraint "
-							 "WHERE contypid = $1 AND contype = 'c' "
+							 "WHERE contypid = $1 "
 							 "ORDER BY conname");
 
 		ExecuteSqlStatement(fout, query->data);
@@ -8282,6 +8284,7 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
 	i_oid = PQfnumber(res, "oid");
 	i_conname = PQfnumber(res, "conname");
 	i_consrc = PQfnumber(res, "consrc");
+	i_contype = PQfnumber(res, "contype");
 
 	constrinfo = (ConstraintInfo *) pg_malloc(ntups * sizeof(ConstraintInfo));
 
@@ -8300,7 +8303,7 @@ getDomainConstraints(Archive *fout, TypeInfo *tyinfo)
 		constrinfo[i].dobj.namespace = tyinfo->dobj.namespace;
 		constrinfo[i].contable = NULL;
 		constrinfo[i].condomain = tyinfo;
-		constrinfo[i].contype = 'c';
+		constrinfo[i].contype = *(PQgetvalue(res, i, i_contype));
 		constrinfo[i].condef = pg_strdup(PQgetvalue(res, i, i_consrc));
 		constrinfo[i].confrelid = InvalidOid;
 		constrinfo[i].conindex = 0;
@@ -12497,8 +12500,18 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo)
 			appendPQExpBuffer(q, " COLLATE %s", fmtQualifiedDumpable(coll));
 	}
 
-	if (typnotnull[0] == 't')
+	if (typnotnull[0] == 't' && fout->remoteVersion < 190000)
 		appendPQExpBufferStr(q, " NOT NULL");
+	else
+	{
+		for (i = 0; i < tyinfo->nDomChecks; i++)
+		{
+			ConstraintInfo *domcheck = &(tyinfo->domChecks[i]);
+
+			if (!domcheck->separate && domcheck->contype == 'n')
+				appendPQExpBufferStr(q, " NOT NULL");
+		}
+	}
 
 	if (typdefault != NULL)
 	{
@@ -12518,7 +12531,7 @@ dumpDomain(Archive *fout, const TypeInfo *tyinfo)
 	{
 		ConstraintInfo *domcheck = &(tyinfo->domChecks[i]);
 
-		if (!domcheck->separate)
+		if (!domcheck->separate && domcheck->contype == 'c')
 			appendPQExpBuffer(q, "\n\tCONSTRAINT %s %s",
 							  fmtId(domcheck->dobj.name), domcheck->condef);
 	}
@@ -18388,7 +18401,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 										  .dropStmt = delq->data));
 		}
 	}
-	else if (coninfo->contype == 'c' && tbinfo == NULL)
+	else if (tbinfo == NULL)
 	{
 		/* CHECK constraint on a domain */
 		TypeInfo   *tyinfo = coninfo->condomain;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 386e21e0c59..71ad355b97c 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1205,6 +1205,22 @@ my %tests = (
 		},
 	},
 
+	'DOMAIN CONSTRAINT NOT NULL / NOT VALID' => {
+		create_sql => 'CREATE DOMAIN dump_test.test_domain_nn AS INT;
+					   ALTER DOMAIN dump_test.test_domain_nn ADD CONSTRAINT nn NOT NULL NOT VALID;',
+		regexp => qr/^
+			\QALTER DOMAIN dump_test.test_domain_nn\E \n^\s+
+			\QADD CONSTRAINT nn NOT NULL NOT VALID;\E
+			/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_post_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	'CONSTRAINT NOT NULL / NOT VALID (child1)' => {
 		regexp => qr/^
 		\QCREATE TABLE dump_test.test_table_nn_chld1 (\E\n
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 1d08268393e..f7c6d56fc01 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -4558,7 +4558,9 @@ listDomains(const char *pattern, bool verbose, bool showSystem)
 					  "       pg_catalog.format_type(t.typbasetype, t.typtypmod) as \"%s\",\n"
 					  "       (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
 					  "        WHERE c.oid = t.typcollation AND bt.oid = t.typbasetype AND t.typcollation <> bt.typcollation) as \"%s\",\n"
-					  "       CASE WHEN t.typnotnull THEN 'not null' END as \"%s\",\n"
+					  "       CASE WHEN t.typnotnull THEN "
+					  "       (SELECT lower(pg_catalog.pg_get_constraintdef(r.oid, true)) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid AND r.contype = " CppAsString2(CONSTRAINT_NOTNULL) ")"
+					  "       END as \"%s\",\n"
 					  "       t.typdefault as \"%s\",\n"
 					  "       pg_catalog.array_to_string(ARRAY(\n"
 					  "         SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid AND r.contype = " CppAsString2(CONSTRAINT_CHECK) " ORDER BY r.conname\n"
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index ba6f05eeb7d..36ab8b5901b 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -926,6 +926,47 @@ ALTER DOMAIN things VALIDATE CONSTRAINT meow;
 ERROR:  column "stuff" of table "thethings" contains values that violate the new constraint
 UPDATE thethings SET stuff = 10;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
+SELECT * FROM thethings ORDER BY 1;
+ stuff 
+-------
+    10
+(1 row)
+
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL;
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --no-op
+ALTER DOMAIN things DROP NOT NULL;
+INSERT INTO thethings VALUES(NULL);
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --ok
+INSERT INTO thethings VALUES(NULL); --error
+ERROR:  domain things does not allow null values
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL; --error
+ERROR:  incompatible NOT VALID constraint "domain_nn" on domain "things"
+HINT:  You might need to validate it using ALTER DOMAIN ... VALIDATE CONSTRAINT.
+ALTER DOMAIN things SET NOT NULL; --error
+ERROR:  column "stuff" of table "thethings" contains null values
+ALTER DOMAIN things ADD CONSTRAINT domain_nn1 NOT NULL NOT VALID; --no-op
+\dD things
+                                      List of domains
+ Schema |  Name  |  Type   | Collation |      Nullable      | Default |       Check        
+--------+--------+---------+-----------+--------------------+---------+--------------------
+ public | things | integer |           | not null not valid |         | CHECK (VALUE < 11)
+(1 row)
+
+SELECT conname, pg_get_constraintdef(oid)
+FROM pg_constraint
+WHERE contypid = 'things'::regtype
+ORDER BY conname COLLATE "C";
+  conname  | pg_get_constraintdef 
+-----------+----------------------
+ domain_nn | NOT NULL NOT VALID
+ meow      | CHECK ((VALUE < 11))
+(2 rows)
+
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --error
+ERROR:  column "stuff" of table "thethings" contains null values
+UPDATE thethings SET stuff = 10 WHERE stuff IS NULL;
+ALTER DOMAIN things SET NOT NULL; --ok
+ALTER DOMAIN things DROP NOT NULL; --ok
 -- Confirm ALTER DOMAIN with RULES.
 create table domtab (col1 integer);
 create domain dom as integer;
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b752a63ab5f..f10b80cc142 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -536,6 +536,28 @@ ALTER DOMAIN things ADD CONSTRAINT meow CHECK (VALUE < 11) NOT VALID;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
 UPDATE thethings SET stuff = 10;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
+SELECT * FROM thethings ORDER BY 1;
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL;
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --no-op
+ALTER DOMAIN things DROP NOT NULL;
+
+INSERT INTO thethings VALUES(NULL);
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --ok
+INSERT INTO thethings VALUES(NULL); --error
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL; --error
+ALTER DOMAIN things SET NOT NULL; --error
+ALTER DOMAIN things ADD CONSTRAINT domain_nn1 NOT NULL NOT VALID; --no-op
+
+\dD things
+SELECT conname, pg_get_constraintdef(oid)
+FROM pg_constraint
+WHERE contypid = 'things'::regtype
+ORDER BY conname COLLATE "C";
+
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --error
+UPDATE thethings SET stuff = 10 WHERE stuff IS NULL;
+ALTER DOMAIN things SET NOT NULL; --ok
+ALTER DOMAIN things DROP NOT NULL; --ok
 
 -- Confirm ALTER DOMAIN with RULES.
 create table domtab (col1 integer);
-- 
2.34.1

#4jian he
jian.universality@gmail.com
In reply to: jian he (#3)
1 attachment(s)
Re: ALTER DOMAIN ADD NOT NULL NOT VALID

hi.
rebase and minor cosmetic change.

Attachments:

v3-0001-ALTER-DOMAIN-ADD-NOT-NULL-NOT-VALID.patchtext/x-patch; charset=US-ASCII; name=v3-0001-ALTER-DOMAIN-ADD-NOT-NULL-NOT-VALID.patchDownload
From b87caa1b2f78d3f1e94e1055219c97e038d2fe61 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 14 Aug 2025 10:40:49 +0800
Subject: [PATCH v3 1/1] ALTER DOMAIN ADD NOT NULL NOT VALID

We have NOT NULL NO VALID for table constraints, we can make domain have NOT
VALID not-null constraint too.

ALTER DOMAIN SET NOT NULL works similarly to its ALTER TABLE counterpart.  It
validates an existing invalidated NOT NULL constraint and set
pg_constraint.convalidated to true.

ALTER DOMAIN ADD NOT NULL will add a new, valid not-null constraint.  If the
domain already has an NOT VALID not-null constraint, the command will result in
an error.
If domain already have VALID not-null, add a NOT VALID is no-op.

\dD changes:
for domain's not null not valid constraint: now column "Nullable" will display as
"not null not valid".
valid domain not-null constraint works as before.

discussed: https://postgr.es/m/CACJufxGcABLgmH951SJkkihK+FW8KR3=odBhXEVCF9atQbur2Q@mail.gmail.com
commitfest entry: https://commitfest.postgresql.org/patch/5768
---
 doc/src/sgml/catalogs.sgml           |   1 +
 doc/src/sgml/ref/alter_domain.sgml   |   1 -
 src/backend/catalog/pg_constraint.c  |  10 +--
 src/backend/commands/typecmds.c      | 122 +++++++++++++++++++++++++--
 src/backend/parser/gram.y            |   8 +-
 src/bin/pg_dump/t/002_pg_dump.pl     |  16 ++++
 src/bin/psql/describe.c              |   4 +-
 src/test/regress/expected/domain.out |  41 +++++++++
 src/test/regress/sql/domain.sql      |  22 +++++
 9 files changed, 205 insertions(+), 20 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index da8a7882580..2f9a506a751 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9521,6 +9521,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        <structfield>typnotnull</structfield> represents a not-null
        constraint on a type.  Used for domains only.
+       Note: the not-null constraint on domain maybe invalidated.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/alter_domain.sgml b/doc/src/sgml/ref/alter_domain.sgml
index 74855172222..4807116eb05 100644
--- a/doc/src/sgml/ref/alter_domain.sgml
+++ b/doc/src/sgml/ref/alter_domain.sgml
@@ -92,7 +92,6 @@ ALTER DOMAIN <replaceable class="parameter">name</replaceable>
       valid using <command>ALTER DOMAIN ... VALIDATE CONSTRAINT</command>.
       Newly inserted or updated rows are always checked against all
       constraints, even those marked <literal>NOT VALID</literal>.
-      <literal>NOT VALID</literal> is only accepted for <literal>CHECK</literal> constraints.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 6002fd0002f..9eca7212556 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -651,8 +651,8 @@ findNotNullConstraint(Oid relid, const char *colname)
 }
 
 /*
- * Find and return the pg_constraint tuple that implements a validated
- * not-null constraint for the given domain.
+ * Find and return the pg_constraint tuple that implements not-null constraint
+ * for the given domain. it may be invalidated.
  */
 HeapTuple
 findDomainNotNullConstraint(Oid typid)
@@ -675,13 +675,9 @@ findDomainNotNullConstraint(Oid typid)
 	{
 		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
 
-		/*
-		 * We're looking for a NOTNULL constraint that's marked validated.
-		 */
+		/* We're looking for a NOT NULL constraint */
 		if (con->contype != CONSTRAINT_NOTNULL)
 			continue;
-		if (!con->convalidated)
-			continue;
 
 		/* Found it */
 		retval = heap_copytuple(conTup);
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 26d985193ae..8b783854da2 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -2765,12 +2765,72 @@ AlterDomainNotNull(List *names, bool notNull)
 	checkDomainOwner(tup);
 
 	/* Is the domain already set to the desired constraint? */
-	if (typTup->typnotnull == notNull)
+	if (!typTup->typnotnull && !notNull)
 	{
 		table_close(typrel, RowExclusiveLock);
 		return address;
 	}
 
+	/*
+	 * We may need validate domain's invalidated NOT NULL constraint
+	 */
+	if (typTup->typnotnull && notNull)
+	{
+		ScanKeyData key[1];
+		SysScanDesc scan;
+		Relation	pg_constraint;
+		Form_pg_constraint copy_con;
+		HeapTuple	conTup;
+		HeapTuple	copyTuple;
+
+		pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+		/* Look for NOT NULL Constraints on this domain */
+		ScanKeyInit(&key[0],
+					Anum_pg_constraint_contypid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(domainoid));
+
+		scan = systable_beginscan(pg_constraint, ConstraintTypidIndexId, true,
+								  NULL, 1, key);
+
+		while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+		{
+			Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+			if (con->contype != CONSTRAINT_NOTNULL)
+				continue;
+
+			/*
+			 * ALTER DOMAIN SET NOT NULL will validate the NOT VALID NOT NULL
+			 * constraint, also set the pg_constraint.convalidated to true.
+			*/
+			if (!con->convalidated)
+			{
+				validateDomainNotNullConstraint(domainoid);
+				copyTuple = heap_copytuple(conTup);
+
+				copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+				copy_con->convalidated = true;
+				CatalogTupleUpdate(pg_constraint, &copyTuple->t_self, copyTuple);
+
+				InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+				heap_freetuple(copyTuple);
+			}
+			break;
+		}
+		systable_endscan(scan);
+		table_close(pg_constraint, AccessShareLock);
+
+		table_close(typrel, RowExclusiveLock);
+
+		/*
+		 * If we already validated the existing NOT VALID NOT NULL constraint
+		 * then we don't need install another not-null constraint, exit now.
+		*/
+		return address;
+	}
+
 	if (notNull)
 	{
 		Constraint *constr;
@@ -2998,9 +3058,51 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
 	}
 	else if (constr->contype == CONSTR_NOTNULL)
 	{
-		/* Is the domain already set NOT NULL? */
+		/*
+		 * Is the domain already set NOT NULL?
+		 * We also need consider domain NOT NULL are invalidated.
+		*/
 		if (typTup->typnotnull)
 		{
+			HeapTuple	conTup;
+			ScanKeyData key[1];
+			SysScanDesc scan;
+			Relation	pg_constraint;
+
+			pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+
+			/* Look for NOT NULL Constraints on this domain */
+			ScanKeyInit(&key[0],
+						Anum_pg_constraint_contypid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(domainoid));
+
+			scan = systable_beginscan(pg_constraint, ConstraintTypidIndexId, true,
+									  NULL, 1, key);
+			while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+			{
+				Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+				if (con->contype != CONSTRAINT_NOTNULL)
+					continue;
+
+				/*
+				 * can not add not-null constraint if the domain already have
+				 * NOT VALID not-null constraint
+				*/
+				if (!constr->skip_validation && !con->convalidated)
+					ereport(ERROR,
+							errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+							errmsg("incompatible NOT VALID constraint \"%s\" on domain \"%s\"",
+									NameStr(con->conname),
+									NameStr(typTup->typname)),
+							errhint("You might need to validate it using %s.",
+									"ALTER DOMAIN ... VALIDATE CONSTRAINT"));
+				break;
+			}
+			systable_endscan(scan);
+			table_close(pg_constraint, AccessShareLock);
+
 			table_close(typrel, RowExclusiveLock);
 			return address;
 		}
@@ -3089,16 +3191,20 @@ AlterDomainValidateConstraint(List *names, const char *constrName)
 						constrName, TypeNameToString(typename))));
 
 	con = (Form_pg_constraint) GETSTRUCT(tuple);
-	if (con->contype != CONSTRAINT_CHECK)
+	if (con->contype != CONSTRAINT_CHECK && con->contype != CONSTRAINT_NOTNULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("constraint \"%s\" of domain \"%s\" is not a check constraint",
+				 errmsg("constraint \"%s\" of domain \"%s\" is not a check or not-null constraint",
 						constrName, TypeNameToString(typename))));
 
-	val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin);
-	conbin = TextDatumGetCString(val);
-
-	validateDomainCheckConstraint(domainoid, conbin);
+	if (con->contype == CONSTRAINT_CHECK)
+	{
+		val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin);
+		conbin = TextDatumGetCString(val);
+		validateDomainCheckConstraint(domainoid, conbin);
+	}
+	else
+		validateDomainNotNullConstraint(domainoid);
 
 	/*
 	 * Now update the catalog, while we have the door open.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index db43034b9db..2a53dfb2f11 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4431,11 +4431,13 @@ DomainConstraintElem:
 					n->contype = CONSTR_NOTNULL;
 					n->location = @1;
 					n->keys = list_make1(makeString("value"));
-					/* no NOT VALID, NO INHERIT support */
+					/* NO INHERIT is not supported */
 					processCASbits($3, @3, "NOT NULL",
 								   NULL, NULL, NULL,
-								   NULL, NULL, yyscanner);
-					n->initially_valid = true;
+								   &n->skip_validation,
+								   NULL, yyscanner);
+					n->is_enforced = true;
+					n->initially_valid = !n->skip_validation;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index e7a2d64f741..837fb9ad00e 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1238,6 +1238,22 @@ my %tests = (
 		},
 	},
 
+	'DOMAIN CONSTRAINT NOT NULL / NOT VALID' => {
+		create_sql => 'CREATE DOMAIN dump_test.test_domain_nn AS INT;
+					   ALTER DOMAIN dump_test.test_domain_nn ADD CONSTRAINT nn NOT NULL NOT VALID;',
+		regexp => qr/^
+			\QALTER DOMAIN dump_test.test_domain_nn\E \n^\s+
+			\QADD CONSTRAINT nn NOT NULL NOT VALID;\E
+			/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_post_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	'CONSTRAINT NOT NULL / NOT VALID (child1)' => {
 		regexp => qr/^
 		\QCREATE TABLE dump_test.test_table_nn_chld1 (\E\n
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 7a06af48842..9309d4a45d4 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -4559,7 +4559,9 @@ listDomains(const char *pattern, bool verbose, bool showSystem)
 					  "       pg_catalog.format_type(t.typbasetype, t.typtypmod) as \"%s\",\n"
 					  "       (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
 					  "        WHERE c.oid = t.typcollation AND bt.oid = t.typbasetype AND t.typcollation <> bt.typcollation) as \"%s\",\n"
-					  "       CASE WHEN t.typnotnull THEN 'not null' END as \"%s\",\n"
+					  "       CASE WHEN t.typnotnull THEN "
+					  "       (SELECT lower(pg_catalog.pg_get_constraintdef(r.oid, true)) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid AND r.contype = " CppAsString2(CONSTRAINT_NOTNULL) ")"
+					  "       END as \"%s\",\n"
 					  "       t.typdefault as \"%s\",\n"
 					  "       pg_catalog.array_to_string(ARRAY(\n"
 					  "         SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid AND r.contype = " CppAsString2(CONSTRAINT_CHECK) " ORDER BY r.conname\n"
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index b5ea707df31..8e74fa991d3 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -926,6 +926,47 @@ ALTER DOMAIN things VALIDATE CONSTRAINT meow;
 ERROR:  column "stuff" of table "thethings" contains values that violate the new constraint
 UPDATE thethings SET stuff = 10;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
+SELECT * FROM thethings ORDER BY 1;
+ stuff 
+-------
+    10
+(1 row)
+
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL;
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --no-op
+ALTER DOMAIN things DROP NOT NULL;
+INSERT INTO thethings VALUES(NULL);
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --ok
+INSERT INTO thethings VALUES(NULL); --error
+ERROR:  domain things does not allow null values
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL; --error
+ERROR:  incompatible NOT VALID constraint "domain_nn" on domain "things"
+HINT:  You might need to validate it using ALTER DOMAIN ... VALIDATE CONSTRAINT.
+ALTER DOMAIN things SET NOT NULL; --error
+ERROR:  column "stuff" of table "thethings" contains null values
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --error
+ERROR:  column "stuff" of table "thethings" contains null values
+ALTER DOMAIN things ADD CONSTRAINT domain_nn1 NOT NULL NOT VALID; --no-op
+\dD things
+                                      List of domains
+ Schema |  Name  |  Type   | Collation |      Nullable      | Default |       Check        
+--------+--------+---------+-----------+--------------------+---------+--------------------
+ public | things | integer |           | not null not valid |         | CHECK (VALUE < 11)
+(1 row)
+
+SELECT  conname, pg_get_constraintdef(oid)
+FROM    pg_constraint
+WHERE   contypid = 'things'::regtype
+ORDER BY conname COLLATE "C";
+  conname  | pg_get_constraintdef 
+-----------+----------------------
+ domain_nn | NOT NULL NOT VALID
+ meow      | CHECK ((VALUE < 11))
+(2 rows)
+
+UPDATE thethings SET stuff = 10 WHERE stuff IS NULL;
+ALTER DOMAIN things SET NOT NULL; --ok
+ALTER DOMAIN things DROP NOT NULL;
 -- Confirm ALTER DOMAIN with RULES.
 create table domtab (col1 integer);
 create domain dom as integer;
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..f7a5c822534 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -536,6 +536,28 @@ ALTER DOMAIN things ADD CONSTRAINT meow CHECK (VALUE < 11) NOT VALID;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
 UPDATE thethings SET stuff = 10;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
+SELECT * FROM thethings ORDER BY 1;
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL;
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --no-op
+ALTER DOMAIN things DROP NOT NULL;
+
+INSERT INTO thethings VALUES(NULL);
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --ok
+INSERT INTO thethings VALUES(NULL); --error
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL; --error
+ALTER DOMAIN things SET NOT NULL; --error
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --error
+ALTER DOMAIN things ADD CONSTRAINT domain_nn1 NOT NULL NOT VALID; --no-op
+
+\dD things
+SELECT  conname, pg_get_constraintdef(oid)
+FROM    pg_constraint
+WHERE   contypid = 'things'::regtype
+ORDER BY conname COLLATE "C";
+
+UPDATE thethings SET stuff = 10 WHERE stuff IS NULL;
+ALTER DOMAIN things SET NOT NULL; --ok
+ALTER DOMAIN things DROP NOT NULL;
 
 -- Confirm ALTER DOMAIN with RULES.
 create table domtab (col1 integer);
-- 
2.34.1

#5Kirill Reshke
reshkekirill@gmail.com
In reply to: jian he (#4)
Re: ALTER DOMAIN ADD NOT NULL NOT VALID

On Thu, 14 Aug 2025 at 07:47, jian he <jian.universality@gmail.com> wrote:

hi.
rebase and minor cosmetic change.

Hi!
It appears satisfactory to me.
I have few observations.
One is whether we should now support CREATE DOMAIN ... NOT NULL NOT
VALID syntax? This could be a separate patch though.
Second observation is just a question:

```

reshke=# create domain dd as int;
CREATE DOMAIN
reshke=# create table dt(i int, c dd);
CREATE TABLE
reshke=# insert into dt values(1,null);
INSERT 0 1
reshke=# alter domain dd add constraint c not null not valid ;
ALTER DOMAIN
reshke=# update dt set i = i + 1;
UPDATE 1
reshke=# update dt set i = i + 1, c =null;
ERROR: domain dd does not allow null values
reshke=# table dt;
i | c
---+---
2 |
(1 row)

```

Is this behaviour correct? Meaning first update is successful while
second is not, yet they would produce the same result.

And last is about psql-tab-complete: we now can complete 'alter domain
... add constraint .. not null' with 'not valid'. This also could be
a separate patch.

--
Best regards,
Kirill Reshke

#6Álvaro Herrera
alvherre@kurilemu.de
In reply to: Kirill Reshke (#5)
Re: ALTER DOMAIN ADD NOT NULL NOT VALID

On 2025-Aug-14, Kirill Reshke wrote:

reshke=# create domain dd as int;
CREATE DOMAIN
reshke=# create table dt(i int, c dd);
CREATE TABLE
reshke=# insert into dt values(1,null);
INSERT 0 1
reshke=# alter domain dd add constraint c not null not valid ;
ALTER DOMAIN
reshke=# update dt set i = i + 1;
UPDATE 1

I think what this example is saying, is that for a not-null constraint
on a domain to be really useful, it has to be propagated as a not-null
constraint on all table (and matview) columns that are using that domain
as datatype. In this case the table column remains nullable after
adding the constraint to the domain, which is why no error occurs (and
which IMO is bogus). In your other update

reshke=# update dt set i = i + 1, c =null;
ERROR: domain dd does not allow null values

the error occurs not when the null value is inserted into the column,
but when the value is assigned the domain type.

The 2016 SQL standard says in 4.23.4 Domain Constraints:

A domain constraint is a constraint that is specified for a domain. It
is applied to all columns that are based on that domain, and to all
values cast to that domain.
which supports the idea that have should do this propagation that I
describe.

So what I think should happen here, is that if you do
ALTER DOMAIN foo ADD CONSTRAINT NOT NULL
then we would look up all columns in all tables/matviews that have a
column of that datatype, and create another CONSTRAINT_NOTNULL
pg_constraint row for that column of that table; then verify all
tables/matviews [that don't already have not-null constraints on those
columns] and error out if there's a null value in any of them.

Contrariwise, and most usefully, if you do
ALTER DOMAIN ADD CONSTRAINT NOT NULL NOT VALID
then you add all the constraints on each table column and mark them as
not-valid; no need for any error here. Then the user can validate each
table separately if they want, minimizing time during which tables are
locked.

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

#7jian he
jian.universality@gmail.com
In reply to: Kirill Reshke (#5)
Re: ALTER DOMAIN ADD NOT NULL NOT VALID

On Thu, Aug 14, 2025 at 5:02 PM Kirill Reshke <reshkekirill@gmail.com> wrote:

I have few observations.
One is whether we should now support CREATE DOMAIN ... NOT NULL NOT
VALID syntax? This could be a separate patch though.

in gram.y:

CreateDomainStmt:
CREATE DOMAIN_P any_name opt_as Typename ColQualList
{
CreateDomainStmt *n = makeNode(CreateDomainStmt);

n->domainname = $3;
n->typeName = $5;
SplitColQualList($6, &n->constraints, &n->collClause,
yyscanner);
$$ = (Node *) n;
}
;

ColConstraintElem:
NOT NULL_P opt_no_inherit
{
Constraint *n = makeNode(Constraint);
n->contype = CONSTR_NOTNULL;
n->location = @1;
n->is_no_inherit = $3;
n->is_enforced = true;
n->skip_validation = false;
n->initially_valid = true;
$$ = (Node *) n;
}
| NULL_P
{
Constraint *n = makeNode(Constraint);

n->contype = CONSTR_NULL;
n->location = @1;
$$ = (Node *) n;
}

CREATE DOMAIN use ColConstraintElem.
that's why we do not support syntax:
``create domain t1 as int not null not valid;``

we also do not support column constraints NOT NULL NOT VALID.
Like
``create table t(a int not null not valid);``
will error out.
so I guess it's fine to not support it?

opt_not_valid: NOT VALID { $$ = true; }
| /* EMPTY */ { $$
= false; }
;
ColConstraintElem:
NOT NULL_P opt_no_inherit opt_not_valid
{
Constraint *n = makeNode(Constraint);

n->contype = CONSTR_NOTNULL;
n->location = @1;
n->is_no_inherit = $3;
n->is_enforced = true;
// n->skip_validation = false;
// n->initially_valid = true;
n->skip_validation = $4;

n->initially_valid = !n->skip_validation;

$$ = (Node *) n;
}

the above change will produce error
/usr/bin/bison -Wno-deprecated -o src/backend/parser/gram.c -d
../../Desktop/pg_src/src9/postgres/src/backend/parser/gram.y
../../Desktop/pg_src/src9/postgres/src/backend/parser/gram.y: error:
shift/reduce conflicts: 1 found, 0 expected
../../Desktop/pg_src/src9/postgres/src/backend/parser/gram.y: note:
rerun with option '-Wcounterexamples' to generate conflict
counterexamples

so currently I don't know how to support syntax
``create domain t1 as int not null not valid;``

I also found it's hard to psql-tab-complete for
'alter domain ... add constraint .. not null' with 'not valid'.

#8Kirill Reshke
reshkekirill@gmail.com
In reply to: jian he (#7)
Re: ALTER DOMAIN ADD NOT NULL NOT VALID

On Mon, 18 Aug 2025 at 09:09, jian he <jian.universality@gmail.com> wrote:

CREATE DOMAIN use ColConstraintElem.
that's why we do not support syntax:
``create domain t1 as int not null not valid;``

we also do not support column constraints NOT NULL NOT VALID.
Like
``create table t(a int not null not valid);``
will error out.
so I guess it's fine to not support it?

Indeed.

so currently I don't know how to support syntax
``create domain t1 as int not null not valid;``

Ok.

I also found it's hard to psql-tab-complete for
'alter domain ... add constraint .. not null' with 'not valid'.

I am under the impression it is pretty trivial.
I mean, isn't this enough?

```
/* ALTER DOMAIN <sth> ADD CONSTRAINT <sth> NOT NULL */
else if (Matches("ALTER", "DOMAIN", MatchAny, "ADD", "CONSTRAINT",
MatchAny, "NOT", "NULL"))
COMPLETE_WITH("NOT VALID");
```

Anyway, this is purely optional and can be a separate topic.

I propose to focus on Alvaro's feedback for now.

--
Best regards,
Kirill Reshke

#9jian he
jian.universality@gmail.com
In reply to: Kirill Reshke (#8)
1 attachment(s)
Re: ALTER DOMAIN ADD NOT NULL NOT VALID

hi.

please check the attached latest version.

I did some minor cosmetic changes.
similar to commit 16a0039, we can just use ShareUpdateExclusiveLock to validate
existing not-valid not null constraints

for the below issue (should the last UPDATE fail or not),
I have already created a thread at [1]/messages/by-id/CACJufxE2oFcLmrqDrqJrH5k03fv+v9=+-PBs-mV5WsJ=31XMyw@mail.gmail.com.
--------------
create domain d1 as int;
create table dt1(i int, c d1);
insert into dt1 values(1,2);
alter domain d1 add constraint cc check(value <> 2) not valid;
update dt1 set i = i + 1;
--------------

[1]: /messages/by-id/CACJufxE2oFcLmrqDrqJrH5k03fv+v9=+-PBs-mV5WsJ=31XMyw@mail.gmail.com

Attachments:

v4-0001-ALTER-DOMAIN-ADD-NOT-NULL-NOT-VALID.patchtext/x-patch; charset=US-ASCII; name=v4-0001-ALTER-DOMAIN-ADD-NOT-NULL-NOT-VALID.patchDownload
From 960f621ff2664b6400e9a079c182e501f004e11e Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Wed, 24 Sep 2025 11:07:40 +0800
Subject: [PATCH v4 1/1] ALTER DOMAIN ADD NOT NULL NOT VALID

We already support NOT NULL NO VALID for table constraints, and the same can be
extended to domains by allowing them to have NOT VALID not-null constraints.

ALTER DOMAIN SET NOT NULL: works similar to ALTER TABLE, it will validates an
existing NOT VALID not-null domain constraint and updates
pg_constraint.convalidated to true.

ALTER DOMAIN ADD NOT NULL: creates a new, valid not-null constraint. If the
domain already has a NOT VALID not-null constraint, the command raises an error.

If the domain already has a valid not-null constraint, adding a NOT VALID
not-null constraint is a no-op.

\dD changes:
for domain's not null not valid constraint: now column "Nullable" will display as
"not null not valid".
valid domain not-null constraint works as before.

discussion: https://postgr.es/m/CACJufxGcABLgmH951SJkkihK+FW8KR3=odBhXEVCF9atQbur2Q@mail.gmail.com
relate discussion: https://postgr.es/m/CACJufxE2oFcLmrqDrqJrH5k03fv+v9=+-PBs-mV5WsJ=31XMyw@mail.gmail.com
commitfest entry: https://commitfest.postgresql.org/patch/5768
---
 doc/src/sgml/catalogs.sgml           |   2 +-
 doc/src/sgml/ref/alter_domain.sgml   |   1 -
 src/backend/catalog/pg_constraint.c  |   8 +-
 src/backend/commands/typecmds.c      | 139 ++++++++++++++++++++++++---
 src/backend/parser/gram.y            |   8 +-
 src/bin/pg_dump/t/002_pg_dump.pl     |  16 +++
 src/bin/psql/describe.c              |   4 +-
 src/test/regress/expected/domain.out |  40 ++++++++
 src/test/regress/sql/domain.sql      |  22 +++++
 9 files changed, 214 insertions(+), 26 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e9095bedf21..751c4bb7048 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9545,7 +9545,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       </para>
       <para>
        <structfield>typnotnull</structfield> represents a not-null
-       constraint on a type.  Used for domains only.
+       (possibly invalid) constraint on a type. Used for domains only.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/alter_domain.sgml b/doc/src/sgml/ref/alter_domain.sgml
index 74855172222..4807116eb05 100644
--- a/doc/src/sgml/ref/alter_domain.sgml
+++ b/doc/src/sgml/ref/alter_domain.sgml
@@ -92,7 +92,6 @@ ALTER DOMAIN <replaceable class="parameter">name</replaceable>
       valid using <command>ALTER DOMAIN ... VALIDATE CONSTRAINT</command>.
       Newly inserted or updated rows are always checked against all
       constraints, even those marked <literal>NOT VALID</literal>.
-      <literal>NOT VALID</literal> is only accepted for <literal>CHECK</literal> constraints.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 6002fd0002f..3af1dfdd179 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -651,7 +651,7 @@ findNotNullConstraint(Oid relid, const char *colname)
 }
 
 /*
- * Find and return the pg_constraint tuple that implements a validated
+ * Find and return the pg_constraint tuple that implements (possibly not valid)
  * not-null constraint for the given domain.
  */
 HeapTuple
@@ -675,13 +675,9 @@ findDomainNotNullConstraint(Oid typid)
 	{
 		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
 
-		/*
-		 * We're looking for a NOTNULL constraint that's marked validated.
-		 */
+		/* We're looking for a NOT NULL constraint */
 		if (con->contype != CONSTRAINT_NOTNULL)
 			continue;
-		if (!con->convalidated)
-			continue;
 
 		/* Found it */
 		retval = heap_copytuple(conTup);
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index c6de04819f1..5745ea55277 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -127,7 +127,7 @@ static Oid	findRangeSubOpclass(List *opcname, Oid subtype);
 static Oid	findRangeCanonicalFunction(List *procname, Oid typeOid);
 static Oid	findRangeSubtypeDiffFunction(List *procname, Oid subtype);
 static void validateDomainCheckConstraint(Oid domainoid, const char *ccbin, LOCKMODE lockmode);
-static void validateDomainNotNullConstraint(Oid domainoid);
+static void validateDomainNotNullConstraint(Oid domainoid, LOCKMODE lockmode);
 static List *get_rels_with_domain(Oid domainOid, LOCKMODE lockmode);
 static void checkEnumOwner(HeapTuple tup);
 static char *domainAddCheckConstraint(Oid domainOid, Oid domainNamespace,
@@ -2765,12 +2765,71 @@ AlterDomainNotNull(List *names, bool notNull)
 	checkDomainOwner(tup);
 
 	/* Is the domain already set to the desired constraint? */
-	if (typTup->typnotnull == notNull)
+	if (!typTup->typnotnull && !notNull)
 	{
 		table_close(typrel, RowExclusiveLock);
 		return address;
 	}
 
+	/*
+	 * We may need to validate existing NOT VALID not-null constraint
+	 */
+	if (typTup->typnotnull && notNull)
+	{
+		ScanKeyData key[1];
+		SysScanDesc scan;
+		Relation	pg_constraint;
+		Form_pg_constraint copy_con;
+		HeapTuple	conTup;
+		HeapTuple	copyTuple;
+
+		pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+
+		ScanKeyInit(&key[0],
+					Anum_pg_constraint_contypid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(domainoid));
+
+		scan = systable_beginscan(pg_constraint, ConstraintTypidIndexId, true,
+								  NULL, 1, key);
+
+		while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+		{
+			Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+			if (con->contype != CONSTRAINT_NOTNULL)
+				continue;
+
+			/*
+			 * ALTER DOMAIN SET NOT NULL will validate the existing NOT VALID
+			 * constraint, also set the pg_constraint.convalidated to true.
+			*/
+			if (!con->convalidated)
+			{
+				validateDomainNotNullConstraint(domainoid, ShareUpdateExclusiveLock);
+				copyTuple = heap_copytuple(conTup);
+
+				copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+				copy_con->convalidated = true;
+				CatalogTupleUpdate(pg_constraint, &copyTuple->t_self, copyTuple);
+
+				InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+				heap_freetuple(copyTuple);
+			}
+			break;
+		}
+		systable_endscan(scan);
+		table_close(pg_constraint, AccessShareLock);
+		table_close(typrel, RowExclusiveLock);
+
+		/*
+		 * If the existing NOT VALID not-null constraint has already been
+		 * validated, there is no need to add another one, exit now.
+		*/	
+		return address;
+	}
+
 	if (notNull)
 	{
 		Constraint *constr;
@@ -2784,7 +2843,7 @@ AlterDomainNotNull(List *names, bool notNull)
 								   typTup->typbasetype, typTup->typtypmod,
 								   constr, NameStr(typTup->typname), NULL);
 
-		validateDomainNotNullConstraint(domainoid);
+		validateDomainNotNullConstraint(domainoid, ShareLock);
 	}
 	else
 	{
@@ -2998,18 +3057,57 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
 	}
 	else if (constr->contype == CONSTR_NOTNULL)
 	{
-		/* Is the domain already set NOT NULL? */
+		/* Is the domain already have a NOT NULL constraint? */
 		if (typTup->typnotnull)
 		{
+			HeapTuple	conTup;
+			ScanKeyData key[1];
+			SysScanDesc scan;
+			Relation	pg_constraint;
+
+			pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+
+			ScanKeyInit(&key[0],
+						Anum_pg_constraint_contypid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(domainoid));
+
+			scan = systable_beginscan(pg_constraint, ConstraintTypidIndexId, true,
+									  NULL, 1, key);
+			while (HeapTupleIsValid(conTup = systable_getnext(scan)))
+			{
+				Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+				if (con->contype != CONSTRAINT_NOTNULL)
+					continue;
+
+				/*
+				 * can not add another valid not-null constraint if the domain
+				 * already have a NOT VALID one.
+				*/
+				if (!con->convalidated && constr->initially_valid)
+					ereport(ERROR,
+							errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+							errmsg("incompatible NOT VALID constraint \"%s\" on domain \"%s\"",
+								   NameStr(con->conname),
+								   NameStr(typTup->typname)),
+							errhint("You might need to validate it using %s.",
+									"ALTER DOMAIN ... VALIDATE CONSTRAINT"));
+				break;
+			}
+			systable_endscan(scan);
+			table_close(pg_constraint, AccessShareLock);
 			table_close(typrel, RowExclusiveLock);
+
 			return address;
 		}
+
 		domainAddNotNullConstraint(domainoid, typTup->typnamespace,
 								   typTup->typbasetype, typTup->typtypmod,
 								   constr, NameStr(typTup->typname), constrAddr);
 
 		if (!constr->skip_validation)
-			validateDomainNotNullConstraint(domainoid);
+			validateDomainNotNullConstraint(domainoid, ShareLock);
 
 		typTup->typnotnull = true;
 		CatalogTupleUpdate(typrel, &tup->t_self, tup);
@@ -3089,21 +3187,27 @@ AlterDomainValidateConstraint(List *names, const char *constrName)
 						constrName, TypeNameToString(typename))));
 
 	con = (Form_pg_constraint) GETSTRUCT(tuple);
-	if (con->contype != CONSTRAINT_CHECK)
+	if (con->contype != CONSTRAINT_CHECK && con->contype != CONSTRAINT_NOTNULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("constraint \"%s\" of domain \"%s\" is not a check constraint",
+				 errmsg("constraint \"%s\" of domain \"%s\" is not a check or not-null constraint",
 						constrName, TypeNameToString(typename))));
 
-	val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin);
-	conbin = TextDatumGetCString(val);
-
 	/*
 	 * Locking related relations with ShareUpdateExclusiveLock is ok because
 	 * not-yet-valid constraints are still enforced against concurrent inserts
 	 * or updates.
-	 */
-	validateDomainCheckConstraint(domainoid, conbin, ShareUpdateExclusiveLock);
+	*/
+	if (con->contype == CONSTRAINT_CHECK)
+	{
+		val = SysCacheGetAttrNotNull(CONSTROID, tuple, Anum_pg_constraint_conbin);
+
+		conbin = TextDatumGetCString(val);
+
+		validateDomainCheckConstraint(domainoid, conbin, ShareUpdateExclusiveLock);
+	}
+	else
+		validateDomainNotNullConstraint(domainoid, ShareUpdateExclusiveLock);
 
 	/*
 	 * Now update the catalog, while we have the door open.
@@ -3131,9 +3235,16 @@ AlterDomainValidateConstraint(List *names, const char *constrName)
 
 /*
  * Verify that all columns currently using the domain are not null.
+ *
+ * It is used to validate existing not-null constraint and to add newly created
+ * not-null constraints to a domain.
+ *
+ * The lockmode is used for relations using the domain.  It should be
+ * ShareLock when adding a new not-null to domain.  It can be
+ * ShareUpdateExclusiveLock when validating the existing not-null constraint.
  */
 static void
-validateDomainNotNullConstraint(Oid domainoid)
+validateDomainNotNullConstraint(Oid domainoid, LOCKMODE lockmode)
 {
 	List	   *rels;
 	ListCell   *rt;
@@ -3141,7 +3252,7 @@ validateDomainNotNullConstraint(Oid domainoid)
 	/* Fetch relation list with attributes based on this domain */
 	/* ShareLock is sufficient to prevent concurrent data changes */
 
-	rels = get_rels_with_domain(domainoid, ShareLock);
+	rels = get_rels_with_domain(domainoid, lockmode);
 
 	foreach(rt, rels)
 	{
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9fd48acb1f8..f83c597aaf0 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4432,11 +4432,13 @@ DomainConstraintElem:
 					n->contype = CONSTR_NOTNULL;
 					n->location = @1;
 					n->keys = list_make1(makeString("value"));
-					/* no NOT VALID, NO INHERIT support */
+					/* NO INHERIT is not supported */
 					processCASbits($3, @3, "NOT NULL",
 								   NULL, NULL, NULL,
-								   NULL, NULL, yyscanner);
-					n->initially_valid = true;
+								   &n->skip_validation,
+								   NULL, yyscanner);
+					n->is_enforced = true;
+					n->initially_valid = !n->skip_validation;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index fc5b9b52f80..7d3c914495c 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1284,6 +1284,22 @@ my %tests = (
 		},
 	},
 
+	'DOMAIN CONSTRAINT NOT NULL / NOT VALID' => {
+		create_sql => 'CREATE DOMAIN dump_test.test_domain_nn AS INT;
+					   ALTER DOMAIN dump_test.test_domain_nn ADD CONSTRAINT nn NOT NULL NOT VALID;',
+		regexp => qr/^
+			\QALTER DOMAIN dump_test.test_domain_nn\E \n^\s+
+			\QADD CONSTRAINT nn NOT NULL NOT VALID;\E
+			/xm,
+		like => {
+			%full_runs, %dump_test_schema_runs, section_post_data => 1,
+		},
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	'CONSTRAINT NOT NULL / NOT VALID (child1)' => {
 		regexp => qr/^
 		\QCREATE TABLE dump_test.test_table_nn_chld1 (\E\n
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 4aa793d7de7..f8c20f71506 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -4559,7 +4559,9 @@ listDomains(const char *pattern, bool verbose, bool showSystem)
 					  "       pg_catalog.format_type(t.typbasetype, t.typtypmod) as \"%s\",\n"
 					  "       (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type bt\n"
 					  "        WHERE c.oid = t.typcollation AND bt.oid = t.typbasetype AND t.typcollation <> bt.typcollation) as \"%s\",\n"
-					  "       CASE WHEN t.typnotnull THEN 'not null' END as \"%s\",\n"
+					  "       CASE WHEN t.typnotnull THEN "
+					  "       (SELECT lower(pg_catalog.pg_get_constraintdef(r.oid, true)) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid AND r.contype = " CppAsString2(CONSTRAINT_NOTNULL) ")"
+					  "       END as \"%s\",\n"
 					  "       t.typdefault as \"%s\",\n"
 					  "       pg_catalog.array_to_string(ARRAY(\n"
 					  "         SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE t.oid = r.contypid AND r.contype = " CppAsString2(CONSTRAINT_CHECK) " ORDER BY r.conname\n"
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..e4bd99c8c15 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -927,6 +927,46 @@ ALTER DOMAIN things VALIDATE CONSTRAINT meow;
 ERROR:  column "stuff" of table "thethings" contains values that violate the new constraint
 UPDATE thethings SET stuff = 10;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
+SELECT * FROM thethings ORDER BY 1;
+ stuff 
+-------
+    10
+(1 row)
+
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL;
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --no-op
+ALTER DOMAIN things DROP NOT NULL;
+INSERT INTO thethings VALUES(NULL);
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --ok
+INSERT INTO thethings VALUES(NULL); --error
+ERROR:  domain things does not allow null values
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL; --error
+ERROR:  incompatible NOT VALID constraint "domain_nn" on domain "things"
+HINT:  You might need to validate it using ALTER DOMAIN ... VALIDATE CONSTRAINT.
+ALTER DOMAIN things SET NOT NULL; --error
+ERROR:  column "stuff" of table "thethings" contains null values
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --error
+ERROR:  column "stuff" of table "thethings" contains null values
+ALTER DOMAIN things ADD CONSTRAINT domain_nn1 NOT NULL NOT VALID; --no-op
+\dD things
+                                      List of domains
+ Schema |  Name  |  Type   | Collation |      Nullable      | Default |       Check        
+--------+--------+---------+-----------+--------------------+---------+--------------------
+ public | things | integer |           | not null not valid |         | CHECK (VALUE < 11)
+(1 row)
+
+SELECT  conname, pg_get_constraintdef(oid)
+FROM    pg_constraint
+WHERE   contypid = 'things'::regtype and contype = 'n';
+  conname  | pg_get_constraintdef 
+-----------+----------------------
+ domain_nn | NOT NULL NOT VALID
+(1 row)
+
+UPDATE thethings SET stuff = 10 WHERE stuff IS NULL;
+ALTER DOMAIN things SET NOT NULL; --ok
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --ok
+ALTER DOMAIN things DROP NOT NULL;
 -- Confirm ALTER DOMAIN with RULES.
 create table domtab (col1 integer);
 create domain dom as integer;
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..394f89b13ae 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -536,6 +536,28 @@ ALTER DOMAIN things ADD CONSTRAINT meow CHECK (VALUE < 11) NOT VALID;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
 UPDATE thethings SET stuff = 10;
 ALTER DOMAIN things VALIDATE CONSTRAINT meow;
+SELECT * FROM thethings ORDER BY 1;
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL;
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --no-op
+ALTER DOMAIN things DROP NOT NULL;
+
+INSERT INTO thethings VALUES(NULL);
+ALTER DOMAIN things ADD CONSTRAINT domain_nn NOT NULL NOT VALID; --ok
+INSERT INTO thethings VALUES(NULL); --error
+ALTER DOMAIN things ADD CONSTRAINT nn1 NOT NULL; --error
+ALTER DOMAIN things SET NOT NULL; --error
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --error
+ALTER DOMAIN things ADD CONSTRAINT domain_nn1 NOT NULL NOT VALID; --no-op
+
+\dD things
+SELECT  conname, pg_get_constraintdef(oid)
+FROM    pg_constraint
+WHERE   contypid = 'things'::regtype and contype = 'n';
+
+UPDATE thethings SET stuff = 10 WHERE stuff IS NULL;
+ALTER DOMAIN things SET NOT NULL; --ok
+ALTER DOMAIN things VALIDATE CONSTRAINT domain_nn; --ok
+ALTER DOMAIN things DROP NOT NULL;
 
 -- Confirm ALTER DOMAIN with RULES.
 create table domtab (col1 integer);
-- 
2.34.1