From 697a9568279634b0cf4e000be7850e8845e37ce0 Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Thu, 4 Jun 2026 18:06:01 +0800
Subject: [PATCH v1-s2] Preserve empty-table behavior for domain fast defaults.

Commit a0b6ef29a allowed ALTER TABLE ADD COLUMN to use the
missing-value fast path for domains with non-volatile constraints.
However, while proving that the default can be stored as a missing value,
a domain CHECK expression can throw an error, for example division by
zero.  That changed behavior for freshly empty tables, where the old
rewrite path scanned no rows and therefore did not evaluate the invalid
domain value during ALTER TABLE.

Skip the fast-default probe for constrained domains when the relation has
no heap blocks.  In that case there is no data rewrite to avoid, and using
the rewrite path preserves the previous behavior.

Add regression coverage for an empty table whose domain CHECK expression
throws during evaluation, while keeping the non-empty case as an error.

Author: Chao Li <lic@highgo.com>
---
 src/backend/commands/tablecmds.c           | 13 +++++++++++++
 src/test/regress/expected/fast_default.out | 18 ++++++++++++++++--
 src/test/regress/sql/fast_default.sql      | 17 +++++++++++++++--
 3 files changed, 44 insertions(+), 4 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a1845240a98..d74635d0a23 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -7548,6 +7548,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		bool		has_domain_constraints;
 		bool		has_missing = false;
 		bool		has_volatile = false;
+		bool		skip_fast_default = false;
 
 		/*
 		 * For an identity column, we can't use build_column_default(),
@@ -7599,6 +7600,17 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 				elog(ERROR, "failed to coerce base type to domain");
 		}
 
+		/*
+		 * For constrained domains, a physically empty table does not need the
+		 * fast default optimization.  Use the rewrite path instead,
+		 * preserving the old behavior of not evaluating the domain constraint
+		 * when there are no heap tuples to rewrite.
+		 */
+		if (has_domain_constraints &&
+			rel->rd_rel->relkind == RELKIND_RELATION &&
+			RelationGetNumberOfBlocks(rel) == 0)
+			skip_fast_default = true;
+
 		if (defval)
 		{
 			NewColumnValue *newval;
@@ -7632,6 +7644,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 * such a failure is only raised when the table has rows.
 			 */
 			if (rel->rd_rel->relkind == RELKIND_RELATION &&
+				!skip_fast_default &&
 				!colDef->generated &&
 				!has_volatile &&
 				!contain_volatile_functions((Node *) defval))
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index 5813f1c61a5..1bcc7be43e3 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -322,12 +322,23 @@ CREATE DOMAIN domain5 AS int CHECK(value > 10) DEFAULT 8;
 CREATE DOMAIN domain6 as int CHECK(value > 10) DEFAULT random(min=>11, max=>100);
 CREATE DOMAIN domain7 as int CHECK((value + random(min=>11::int, max=>11)) > 12);
 CREATE DOMAIN domain8 as int NOT NULL;
+CREATE DOMAIN domain9 AS int CHECK(1 / (value - 1) > 0);
 CREATE TABLE test_add_domain_col(a int);
 -- succeeds despite constraint-violating default because table is empty
 ALTER TABLE test_add_domain_col ADD COLUMN a1 domain5;
 NOTICE:  rewriting table test_add_domain_col for reason 2
 ALTER TABLE test_add_domain_col DROP COLUMN a1;
 INSERT INTO test_add_domain_col VALUES(1),(2);
+-- likewise, an empty table succeeds if the domain check expression errors
+CREATE TABLE test_add_domain_col_empty(a int);
+ALTER TABLE test_add_domain_col_empty ADD COLUMN b domain9 DEFAULT 1;
+NOTICE:  rewriting table test_add_domain_col_empty for reason 2
+DROP TABLE test_add_domain_col_empty;
+-- but errors in the default expression itself should not be hidden
+CREATE TABLE test_add_domain_col_bad_default(a int);
+ALTER TABLE test_add_domain_col_bad_default ADD COLUMN b domain5 DEFAULT 1 / 0;
+ERROR:  division by zero
+DROP TABLE test_add_domain_col_bad_default;
 -- tests with non-empty table
 ALTER TABLE test_add_domain_col ADD COLUMN b domain5; -- table rewrite, then fail
 NOTICE:  rewriting table test_add_domain_col for reason 2
@@ -338,6 +349,8 @@ ERROR:  domain domain8 does not allow null values
 ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 1; -- table rewrite, then fail
 NOTICE:  rewriting table test_add_domain_col for reason 2
 ERROR:  value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE test_add_domain_col ADD COLUMN b domain9 DEFAULT 1; -- fast proof fails
+ERROR:  division by zero
 ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 12; -- ok, no table rewrite
 -- explicit column default expression overrides domain's default
 -- expression, so no table rewrite
@@ -365,8 +378,8 @@ ALTER TABLE test_add_domain_col ADD COLUMN f domain7;
 NOTICE:  rewriting table test_add_domain_col for reason 2
 -- domain with both volatile and non-volatile CHECK constraints: the
 -- volatile one forces a table rewrite
-CREATE DOMAIN domain9 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0);
-ALTER TABLE test_add_domain_col ADD COLUMN g domain9 DEFAULT 14;
+CREATE DOMAIN domain10 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0);
+ALTER TABLE test_add_domain_col ADD COLUMN g domain10 DEFAULT 14;
 NOTICE:  rewriting table test_add_domain_col for reason 2
 -- virtual generated columns cannot have domain types
 ALTER TABLE test_add_domain_col ADD COLUMN h domain5
@@ -383,6 +396,7 @@ DROP DOMAIN domain6;
 DROP DOMAIN domain7;
 DROP DOMAIN domain8;
 DROP DOMAIN domain9;
+DROP DOMAIN domain10;
 DROP FUNCTION foo(INT);
 -- Fall back to full rewrite for volatile expressions
 CREATE TABLE T(pk INT NOT NULL PRIMARY KEY);
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index e5d9a3d2fd1..8b58c6d44d0 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -292,6 +292,7 @@ CREATE DOMAIN domain5 AS int CHECK(value > 10) DEFAULT 8;
 CREATE DOMAIN domain6 as int CHECK(value > 10) DEFAULT random(min=>11, max=>100);
 CREATE DOMAIN domain7 as int CHECK((value + random(min=>11::int, max=>11)) > 12);
 CREATE DOMAIN domain8 as int NOT NULL;
+CREATE DOMAIN domain9 AS int CHECK(1 / (value - 1) > 0);
 
 CREATE TABLE test_add_domain_col(a int);
 -- succeeds despite constraint-violating default because table is empty
@@ -299,10 +300,21 @@ ALTER TABLE test_add_domain_col ADD COLUMN a1 domain5;
 ALTER TABLE test_add_domain_col DROP COLUMN a1;
 INSERT INTO test_add_domain_col VALUES(1),(2);
 
+-- likewise, an empty table succeeds if the domain check expression errors
+CREATE TABLE test_add_domain_col_empty(a int);
+ALTER TABLE test_add_domain_col_empty ADD COLUMN b domain9 DEFAULT 1;
+DROP TABLE test_add_domain_col_empty;
+
+-- but errors in the default expression itself should not be hidden
+CREATE TABLE test_add_domain_col_bad_default(a int);
+ALTER TABLE test_add_domain_col_bad_default ADD COLUMN b domain5 DEFAULT 1 / 0;
+DROP TABLE test_add_domain_col_bad_default;
+
 -- tests with non-empty table
 ALTER TABLE test_add_domain_col ADD COLUMN b domain5; -- table rewrite, then fail
 ALTER TABLE test_add_domain_col ADD COLUMN b domain8; -- table rewrite, then fail
 ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 1; -- table rewrite, then fail
+ALTER TABLE test_add_domain_col ADD COLUMN b domain9 DEFAULT 1; -- fast proof fails
 ALTER TABLE test_add_domain_col ADD COLUMN b domain5 DEFAULT 12; -- ok, no table rewrite
 
 -- explicit column default expression overrides domain's default
@@ -325,8 +337,8 @@ ALTER TABLE test_add_domain_col ADD COLUMN f domain7;
 
 -- domain with both volatile and non-volatile CHECK constraints: the
 -- volatile one forces a table rewrite
-CREATE DOMAIN domain9 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0);
-ALTER TABLE test_add_domain_col ADD COLUMN g domain9 DEFAULT 14;
+CREATE DOMAIN domain10 AS int CHECK(value > 10) CHECK((value + random(min=>1::int, max=>1)) > 0);
+ALTER TABLE test_add_domain_col ADD COLUMN g domain10 DEFAULT 14;
 
 -- virtual generated columns cannot have domain types
 ALTER TABLE test_add_domain_col ADD COLUMN h domain5
@@ -343,6 +355,7 @@ DROP DOMAIN domain6;
 DROP DOMAIN domain7;
 DROP DOMAIN domain8;
 DROP DOMAIN domain9;
+DROP DOMAIN domain10;
 DROP FUNCTION foo(INT);
 
 -- Fall back to full rewrite for volatile expressions
-- 
2.50.1 (Apple Git-155)

