From ababce3f5c6d767b893b6e93067f5b506cc590f3 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Thu, 9 Jul 2026 10:13:56 +0800
Subject: [PATCH v7 1/1] Skipping table rewrites for changing column types in
 some cases

If a table rewrite is required, there's nothing we can do about it.
We can add a new boolean field need_compute to NewColumnValue.  This field is
currently set to true only when changing an existing column's type to a
constrained domain.  In such cases, a table scan alone is sufficient.

discussion: https://postgr.es/m/CACJufxFX0DupbF5+DBNF3mXCFNTZ1Y7jpT11+tCD_FcyADHs2A@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/5907
---
 doc/src/sgml/ref/alter_table.sgml           |   5 +-
 src/backend/commands/copyfrom.c             |   2 +-
 src/backend/commands/tablecmds.c            | 108 ++++++++++++++++++--
 src/backend/executor/execExpr.c             |   2 +-
 src/backend/optimizer/util/clauses.c        |  39 ++++++-
 src/backend/parser/parse_expr.c             |   2 +-
 src/backend/utils/cache/typcache.c          |  45 +++++++-
 src/include/optimizer/optimizer.h           |   1 +
 src/include/utils/typcache.h                |   3 +-
 src/test/regress/expected/event_trigger.out |  13 +++
 src/test/regress/expected/fast_default.out  |  41 ++++++++
 src/test/regress/sql/event_trigger.sql      |  11 ++
 src/test/regress/sql/fast_default.sql       |  30 ++++++
 13 files changed, 285 insertions(+), 17 deletions(-)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index ff7071bef5b..e5c8ff47027 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1720,7 +1720,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     if the <literal>USING</literal> clause does not change the column
     contents and the old type is either binary coercible to the new type
     or an unconstrained domain over the new type, a table rewrite is not
-    needed.  However, indexes will still be rebuilt unless the system
+    needed. If the new type is a constrained domain over the old type and
+    domain's CHECK constraint consists only of IMMUTABLE build-in functions,
+    table rewrite is not needed, however table scan is required.
+    Even if table rewrite is not needed, indexes will still be rebuilt unless the system
     can verify that the new index would be logically equivalent to the
     existing one.  For example, if the collation for a column has been
     changed, an index rebuild is required because the new sort
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 80a527ed4c6..7bb9530888f 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -1650,7 +1650,7 @@ BeginCopyFrom(ParseState *pstate,
 		{
 			Form_pg_attribute att = TupleDescAttr(tupDesc, attno - 1);
 
-			cstate->domain_with_constraint[attno - 1] = DomainHasConstraints(att->atttypid, NULL);
+			cstate->domain_with_constraint[attno - 1] = DomainHasConstraints(att->atttypid, NULL, NULL);
 		}
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 95abaf4890c..d532c47ac1f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -234,6 +234,12 @@ typedef struct NewConstraint
  * are just copied from the old table during ATRewriteTable.  Note that the
  * expr is an expression over *old* table values, except when is_generated
  * is true; then it is an expression over columns of the *new* tuple.
+ *
+ * If need_compute is true, using table scan to evaluate the new column value in
+ * Phase 3, no table rewrite is required.  Currently, this is only used in the
+ * ALTER COLUMN SET DATA TYPE command, where the column’s data type is being
+ * changed to a constrained domain. need_compute is irrelevant in case of table
+ * rewrite.
  */
 typedef struct NewColumnValue
 {
@@ -241,6 +247,7 @@ typedef struct NewColumnValue
 	Expr	   *expr;			/* expression to compute */
 	ExprState  *exprstate;		/* execution state */
 	bool		is_generated;	/* is it a GENERATED expression? */
+	bool		need_compute;
 } NewColumnValue;
 
 /*
@@ -688,7 +695,7 @@ static void ATPrepAlterColumnType(List **wqueue,
 								  bool recurse, bool recursing,
 								  AlterTableCmd *cmd, LOCKMODE lockmode,
 								  AlterTableUtilityContext *context);
-static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno);
+static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno, bool *scan_only);
 static ObjectAddress ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 										   AlterTableCmd *cmd, LOCKMODE lockmode);
 static void RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
@@ -6138,7 +6145,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * rebuild data.
 			 */
 			if (tab->constraints != NIL || tab->verify_new_notnull ||
-				tab->partition_constraint != NULL)
+				tab->partition_constraint != NULL || tab->newvals)
 				ATRewriteTable(tab, InvalidOid);
 
 			/*
@@ -6333,6 +6340,9 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 
 		/* expr already planned */
 		ex->exprstate = ExecInitExpr(ex->expr, NULL);
+
+		if (ex->need_compute)
+			needscan = true;
 	}
 
 	notnull_attrs = notnull_virtual_attrs = NIL;
@@ -6562,6 +6572,48 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 				 * new constraints etc.
 				 */
 				insertslot = oldslot;
+
+				/*
+				 * To safely evaluate the NewColumnValue expression against
+				 * the original table slot, temporarily switch oldslot's
+				 * tts_tupleDescriptor to oldTupDesc, and then switch it back
+				 * to newTupDesc afterwards.
+				 *
+				 * Rationale: 1. If there is no table rewrite, the contents of
+				 * oldslot are guaranteed to be fully compatible with
+				 * oldTupDesc. See the comments in slot_deform_heap_tuple
+				 * regarding slot_getmissingattrs.
+				 *
+				 * 2. Passing the updated newTupDesc into ExecEvalExpr would
+				 * cause CheckVarSlotCompatibility to fail, we have to
+				 * temporarily use oldTupDesc
+				 */
+				if (tab->newvals != NIL)
+				{
+					bool		isnull pg_attribute_unused();
+
+					insertslot->tts_tupleDescriptor = oldTupDesc;
+					econtext->ecxt_scantuple = insertslot;
+
+					foreach(l, tab->newvals)
+					{
+						NewColumnValue *ex = lfirst(l);
+
+						if (!ex->need_compute)
+							continue;
+
+						/*
+						 * ExecEvalExprNoReturn cannot be used here because
+						 * the expression was compiled via ExecInitExpr. We
+						 * only need to check if oldslot satisfies new
+						 * expression (NewColumnValue->expr), so it's fine to
+						 * ignore value returned by ExecEvalExpr.
+						 */
+						(void) ExecEvalExpr(ex->exprstate, econtext, &isnull);
+					}
+
+					insertslot->tts_tupleDescriptor = newTupDesc;
+				}
 			}
 
 			/* Now check any constraints on the possibly-changed tuple */
@@ -7624,7 +7676,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			defval = (Expr *) build_column_default(rel, attribute->attnum);
 
 		/* Build CoerceToDomain(NULL) expression if needed */
-		has_domain_constraints = DomainHasConstraints(attribute->atttypid, NULL);
+		has_domain_constraints = DomainHasConstraints(attribute->atttypid, NULL, NULL);
 		if (!defval && has_domain_constraints)
 		{
 			Oid			baseTypeId;
@@ -15000,6 +15052,8 @@ ATPrepAlterColumnType(List **wqueue,
 	else if (tab->relkind == RELKIND_RELATION ||
 			 tab->relkind == RELKIND_PARTITIONED_TABLE)
 	{
+		bool		scan_only = false;
+
 		/*
 		 * Set up an expression to transform the old data value to the new
 		 * type. If a USING option was given, use the expression as
@@ -15064,9 +15118,15 @@ ATPrepAlterColumnType(List **wqueue,
 		newval->expr = (Expr *) transform;
 		newval->is_generated = false;
 
-		tab->newvals = lappend(tab->newvals, newval);
-		if (ATColumnChangeRequiresRewrite(transform, attnum))
+		if (ATColumnChangeRequiresRewrite(transform, attnum, &scan_only))
+		{
 			tab->rewrite |= AT_REWRITE_COLUMN_REWRITE;
+			newval->need_compute = true;
+		}
+		else if (!tab->rewrite && scan_only)
+			newval->need_compute = true;
+
+		tab->newvals = lappend(tab->newvals, newval);
 	}
 	else if (transform)
 		ereport(ERROR,
@@ -15201,11 +15261,12 @@ ATPrepAlterColumnType(List **wqueue,
  * - {NEW,OLD} or {OLD,NEW} is {timestamptz,timestamp} and the timezone is UTC
  *
  * In the case of a constrained domain, we could get by with scanning the
- * table and checking the constraint rather than actually rewriting it, but we
- * don't currently try to do that.
+ * table and checking the constraint rather than actually rewriting it.
+ * However, this is only valid when the constraint contains immutable built-in
+ * functions. In that case, scan_only is set to true.
  */
 static bool
-ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno)
+ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno, bool *scan_only)
 {
 	Assert(expr != NULL);
 
@@ -15218,10 +15279,39 @@ ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno)
 			expr = (Node *) ((RelabelType *) expr)->arg;
 		else if (IsA(expr, CoerceToDomain))
 		{
+			bool		has_mutable;
+			bool		has_constraints;
+
 			CoerceToDomain *d = (CoerceToDomain *) expr;
 
-			if (DomainHasConstraints(d->resulttype, NULL))
+			has_constraints = DomainHasConstraints(d->resulttype, NULL, &has_mutable);
+
+			if (has_mutable)
 				return true;
+			else if (has_constraints)
+			{
+				HeapTuple	typtup;
+				Form_pg_type typform;
+
+				typtup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(d->resulttype));
+				if (!HeapTupleIsValid(typtup))
+					elog(ERROR, "cache lookup failed for type %u", d->resulttype);
+
+				typform = (Form_pg_type) GETSTRUCT(typtup);
+
+				if (typform->typbasetype >= FirstUnpinnedObjectId ||
+					DomainConstrContainUserDefinedFunc(d->resulttype))
+				{
+					ReleaseSysCache(typtup);
+
+					return true;
+				}
+				ReleaseSysCache(typtup);
+
+				if (scan_only)
+					*scan_only = true;
+			}
+
 			expr = (Node *) d->arg;
 		}
 		else if (IsA(expr, FuncExpr))
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index cfea7e160c2..bf7f9eb6f73 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -5063,6 +5063,6 @@ ExecInitJsonCoercion(ExprState *state, JsonReturning *returning,
 	scratch.d.jsonexpr_coercion.exists_cast_to_int = exists_coerce &&
 		getBaseType(returning->typid) == INT4OID;
 	scratch.d.jsonexpr_coercion.exists_check_domain = exists_coerce &&
-		DomainHasConstraints(returning->typid, NULL);
+		DomainHasConstraints(returning->typid, NULL, NULL);
 	ExprEvalPushStep(state, &scratch);
 }
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index aa8886ec210..8f4f31ac8b2 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -116,6 +116,7 @@ static bool find_window_functions_walker(Node *node, WindowFuncLists *lists);
 static bool contain_subplans_walker(Node *node, void *context);
 static bool contain_mutable_functions_walker(Node *node, void *context);
 static bool contain_volatile_functions_walker(Node *node, void *context);
+static bool contain_user_defined_functions_walker(Node *node, void *context);
 static bool contain_volatile_functions_not_nextval_walker(Node *node, void *context);
 static bool max_parallel_hazard_walker(Node *node,
 									   max_parallel_hazard_context *context);
@@ -666,6 +667,42 @@ contain_volatile_functions_walker(Node *node, void *context)
 								  context);
 }
 
+/*
+ * user_defined_func_checker
+ *		check_functions_in_node callback: true if funcid is user-defined.
+ */
+static bool
+user_defined_func_checker(Oid funcid, void *context)
+{
+	return (funcid >= FirstUnpinnedObjectId);
+}
+
+bool
+contain_user_defined_functions(Node *clause)
+{
+	return contain_user_defined_functions_walker(clause, NULL);
+}
+
+static bool
+contain_user_defined_functions_walker(Node *node, void *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (exprType(node) >= FirstUnpinnedObjectId)
+		return false;
+
+	if (check_functions_in_node(node, user_defined_func_checker, NULL))
+		return false;
+
+	if (IsA(node, NextValueExpr))
+		return false;
+
+	return expression_tree_walker(node,
+								  contain_user_defined_functions_walker,
+								  context);
+}
+
 /*
  * contain_volatile_functions_after_planning
  *	  Test whether given expression contains volatile functions.
@@ -4073,7 +4110,7 @@ eval_const_expressions_mutator(Node *node,
 				arg = eval_const_expressions_mutator((Node *) cdomain->arg,
 													 context);
 				if (context->estimate ||
-					!DomainHasConstraints(cdomain->resulttype, NULL))
+					!DomainHasConstraints(cdomain->resulttype, NULL, NULL))
 				{
 					/* Record dependency, if this isn't estimation mode */
 					if (context->root && !context->estimate)
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e6ea34a7809..3997bedbcc2 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -4749,7 +4749,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
 			if (jsexpr->returning->typid != TEXTOID)
 			{
 				if (get_typtype(jsexpr->returning->typid) == TYPTYPE_DOMAIN &&
-					DomainHasConstraints(jsexpr->returning->typid, NULL))
+					DomainHasConstraints(jsexpr->returning->typid, NULL, NULL))
 					jsexpr->use_json_coercion = true;
 				else
 					jsexpr->use_io_coercion = true;
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 650b5d9bad9..bdee1b349d7 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -1489,12 +1489,14 @@ UpdateDomainConstraintRef(DomainConstraintRef *ref)
  *
  * Returns true if the domain has any constraints at all.  If has_volatile
  * is not NULL, also checks whether any CHECK constraint contains a volatile
- * expression and sets *has_volatile accordingly.
+ * expression and sets *has_volatile accordingly. If has_mutable
+ * is not NULL, also checks whether any CHECK constraint contains a mutable
+ * expression and sets *has_mutable accordingly.
  *
  * This is defined to return false, not fail, if type is not a domain.
  */
 bool
-DomainHasConstraints(Oid type_id, bool *has_volatile)
+DomainHasConstraints(Oid type_id, bool *has_volatile, bool *has_mutable)
 {
 	TypeCacheEntry *typentry;
 
@@ -1523,9 +1525,48 @@ DomainHasConstraints(Oid type_id, bool *has_volatile)
 		}
 	}
 
+	if (has_mutable)
+	{
+		*has_mutable = false;
+
+		foreach_node(DomainConstraintState, constrstate,
+					 typentry->domainData->constraints)
+		{
+			if (constrstate->constrainttype == DOM_CONSTRAINT_CHECK &&
+				contain_mutable_functions((Node *) constrstate->check_expr))
+			{
+				*has_mutable = true;
+				break;
+			}
+		}
+	}
+
 	return true;
 }
 
+bool
+DomainConstrContainUserDefinedFunc(Oid type_id)
+{
+	TypeCacheEntry *typentry;
+
+	typentry = lookup_type_cache(type_id, TYPECACHE_DOMAIN_CONSTR_INFO);
+
+	if (typentry->domainData == NULL)
+		return false;
+
+	foreach_node(DomainConstraintState, constrstate,
+					typentry->domainData->constraints)
+	{
+		if (constrstate->constrainttype == DOM_CONSTRAINT_CHECK &&
+			contain_user_defined_functions((Node *) constrstate->check_expr))
+		{
+			return true;
+		}
+	}
+
+	return false;
+}
+
 
 /*
  * array_element_has_equality and friends are helper routines to check
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index cb6241e2bdd..43baeaa9e06 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -143,6 +143,7 @@ extern bool contain_mutable_functions_after_planning(Expr *expr);
 extern bool contain_volatile_functions(Node *clause);
 extern bool contain_volatile_functions_after_planning(Expr *expr);
 extern bool contain_volatile_functions_not_nextval(Node *clause);
+extern bool contain_user_defined_functions(Node *clause);
 
 extern Node *eval_const_expressions(PlannerInfo *root, Node *node);
 
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index 5a4aa9ec840..e1db77dff1c 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -183,7 +183,8 @@ extern void InitDomainConstraintRef(Oid type_id, DomainConstraintRef *ref,
 
 extern void UpdateDomainConstraintRef(DomainConstraintRef *ref);
 
-extern bool DomainHasConstraints(Oid type_id, bool *has_volatile);
+extern bool DomainHasConstraints(Oid type_id, bool *has_volatile, bool *has_mutable);
+extern bool DomainConstrContainUserDefinedFunc(Oid type_id);
 
 extern TupleDesc lookup_rowtype_tupdesc(Oid type_id, int32 typmod);
 
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 86ae50ce531..331bacfb054 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -631,13 +631,26 @@ DROP MATERIALIZED VIEW heapmv;
 -- shouldn't trigger a table_rewrite event
 alter table rewriteme alter column foo type numeric(12,4);
 begin;
+create domain d_ts as timestamp check(value is null);
+create domain d_tsnn as timestamp check(value is not null);
+create domain d_tstz as timestamptz check(value is null);
+savepoint s1;
+set timezone to 'UTC';
+alter table rewriteme alter column bar type d_tsnn; -- error, no table rewrite
+ERROR:  value for domain d_tsnn violates check constraint "d_tsnn_check"
+rollback to savepoint s1;
 set timezone to 'UTC';
 alter table rewriteme alter column bar type timestamp;
+alter table rewriteme alter column bar type d_ts;
 set timezone to '0';
 alter table rewriteme alter column bar type timestamptz;
+alter table rewriteme alter column bar type d_tstz;
+alter table rewriteme add column anotherone timestamptz;
 set timezone to 'Europe/London';
 alter table rewriteme alter column bar type timestamp; -- does rewrite
 NOTICE:  Table 'rewriteme' is being rewritten (reason = 4)
+alter table rewriteme alter column anotherone type d_ts; -- does rewrite
+NOTICE:  Table 'rewriteme' is being rewritten (reason = 4)
 rollback;
 -- typed tables are rewritten when their type changes.  Don't emit table
 -- name, because firing order is not stable.
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index 49485f5ec6a..f2b83220a2c 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -323,6 +323,47 @@ DROP DOMAIN domain2;
 DROP DOMAIN domain3;
 DROP DOMAIN domain4;
 DROP FUNCTION foo(INT);
+-- Test changing column data type to constrained domain
+CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain2 AS domain1 CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain3 AS domain1 CHECK(VALUE > random(min=>10, max=>10)) CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain4 AS TEXT COLLATE "POSIX" CHECK (value <> 'hello');
+CREATE DOMAIN domain5 AS int[] NOT NULL;
+CREATE DOMAIN domain6 AS varchar COLLATE "POSIX" CHECK (value <> 'hello');
+CREATE TABLE t22(a INT, b INT, c text COLLATE "C", col1 INT[]);
+INSERT INTO t22 VALUES(-2, -1, 'hello', NULL);
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2; -- table rewrite
+NOTICE:  rewriting table t22 for reason 4
+ERROR:  value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain3; -- table rewrite
+NOTICE:  rewriting table t22 for reason 4
+ERROR:  value for domain domain3 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE domain4; -- no table rewrite
+ERROR:  value for domain domain4 violates check constraint "domain4_check"
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE domain6; -- no table rewrite
+ERROR:  value for domain domain6 violates check constraint "domain6_check"
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain5 USING col1::int4[]::domain5; -- no table rewrite
+ERROR:  domain domain5 does not allow null values
+UPDATE t22 set col1 = '{-2,null}';
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain5 USING col1::int4[]::domain5; -- no table rewrite
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1; -- no table rewrite
+ERROR:  value for domain domain1 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN b SET DATA TYPE domain2 USING 15; -- table rewrite, ok
+NOTICE:  rewriting table t22 for reason 4
+ALTER TABLE t22 DROP COLUMN a, DROP COLUMN c, ALTER COLUMN b SET DATA TYPE domain1, ADD COLUMN col2 INT DEFAULT 11; -- no table rewrite, ok
+CREATE FUNCTION domain_test() RETURNS bool language plpgsql AS $$ BEGIN DELETE FROM t22; return true; END$$;
+CREATE DOMAIN domain7 AS INT CHECK (domain_test());
+ALTER TABLE t22 ALTER COLUMN b SET DATA TYPE domain7; -- table rewrite
+NOTICE:  rewriting table t22 for reason 4
+TABLE t22;
+ b  |   col1    | col2 
+----+-----------+------
+ 15 | {-2,NULL} |   11
+(1 row)
+
+DROP TABLE t22;
+DROP DOMAIN domain1, domain2, domain3, domain4, domain5, domain6, domain7;
+DROP FUNCTION domain_test();
 -- Fall back to full rewrite for volatile expressions
 CREATE TABLE T(pk INT NOT NULL PRIMARY KEY);
 INSERT INTO T VALUES (1);
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index d0e6ba295fe..401bac8b0f9 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -460,12 +460,23 @@ DROP MATERIALIZED VIEW heapmv;
 -- shouldn't trigger a table_rewrite event
 alter table rewriteme alter column foo type numeric(12,4);
 begin;
+create domain d_ts as timestamp check(value is null);
+create domain d_tsnn as timestamp check(value is not null);
+create domain d_tstz as timestamptz check(value is null);
+savepoint s1;
+set timezone to 'UTC';
+alter table rewriteme alter column bar type d_tsnn; -- error, no table rewrite
+rollback to savepoint s1;
 set timezone to 'UTC';
 alter table rewriteme alter column bar type timestamp;
+alter table rewriteme alter column bar type d_ts;
 set timezone to '0';
 alter table rewriteme alter column bar type timestamptz;
+alter table rewriteme alter column bar type d_tstz;
+alter table rewriteme add column anotherone timestamptz;
 set timezone to 'Europe/London';
 alter table rewriteme alter column bar type timestamp; -- does rewrite
+alter table rewriteme alter column anotherone type d_ts; -- does rewrite
 rollback;
 
 -- typed tables are rewritten when their type changes.  Don't emit table
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index ccd138c4efc..b178edb592b 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -294,6 +294,36 @@ DROP DOMAIN domain3;
 DROP DOMAIN domain4;
 DROP FUNCTION foo(INT);
 
+-- Test changing column data type to constrained domain
+CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain2 AS domain1 CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain3 AS domain1 CHECK(VALUE > random(min=>10, max=>10)) CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain4 AS TEXT COLLATE "POSIX" CHECK (value <> 'hello');
+CREATE DOMAIN domain5 AS int[] NOT NULL;
+CREATE DOMAIN domain6 AS varchar COLLATE "POSIX" CHECK (value <> 'hello');
+CREATE TABLE t22(a INT, b INT, c text COLLATE "C", col1 INT[]);
+INSERT INTO t22 VALUES(-2, -1, 'hello', NULL);
+
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2; -- table rewrite
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain3; -- table rewrite
+
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE domain4; -- no table rewrite
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE domain6; -- no table rewrite
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain5 USING col1::int4[]::domain5; -- no table rewrite
+UPDATE t22 set col1 = '{-2,null}';
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain5 USING col1::int4[]::domain5; -- no table rewrite
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1; -- no table rewrite
+ALTER TABLE t22 ALTER COLUMN b SET DATA TYPE domain2 USING 15; -- table rewrite, ok
+ALTER TABLE t22 DROP COLUMN a, DROP COLUMN c, ALTER COLUMN b SET DATA TYPE domain1, ADD COLUMN col2 INT DEFAULT 11; -- no table rewrite, ok
+
+CREATE FUNCTION domain_test() RETURNS bool language plpgsql AS $$ BEGIN DELETE FROM t22; return true; END$$;
+CREATE DOMAIN domain7 AS INT CHECK (domain_test());
+ALTER TABLE t22 ALTER COLUMN b SET DATA TYPE domain7; -- table rewrite
+TABLE t22;
+DROP TABLE t22;
+DROP DOMAIN domain1, domain2, domain3, domain4, domain5, domain6, domain7;
+DROP FUNCTION domain_test();
+
 -- Fall back to full rewrite for volatile expressions
 CREATE TABLE T(pk INT NOT NULL PRIMARY KEY);
 
-- 
2.34.1

