On Sun, Mar 15, 2026 at 12:32 AM Viktor Holmberg <[email protected]> wrote:
>
> As I’m sure you know Jian this needs a rebase now that a0b6ef29a518 has been
> merged. It’s a bit hard for me to review in this state when I can’t apply the
> patches cleanly.
>
> + /*
> + * we can not use ExecEvalExprNoReturn here, because we
> + * use ExecInitExpr compile NewColumnValue->expr. Here,
> + * we only check whether the oldslot value satisfies the
> + * domain constraint. So it is ok to override the value
> + * evaluated by ExecEvalExpr.
> + */
> + values = ExecEvalExpr(ex->exprstate, econtext, &isnull);
> + values = (Datum) 0;
> + isnull = true;
>
> I don’t understand this piece of code, and why value is re-assigned right
> away. Not saying it’s wrong but if you could explain why it is like that to
> someone not well versed in C. Would something like (void)
> ExecEvalExpr(ex->exprstate, econtext, &isnull); do?
>
CREATE TABLE t(a int);
INSERT INTO t VALUES (1);
CREATE DOMAIN d1 AS INT check (value <> 1);
ALTER TABLE t ALTER COLUMN a SET DATA TYPE d1;
ATPrepAlterColumnType->coerce_to_target_type will return a COERCETODOMAIN Node
{COERCETODOMAIN
:arg
{VAR
:varno 1
:varattno 1
:vartype 23
:vartypmod -1
:varcollid 0
:varnullingrels (b)
:varlevelsup 0
:varreturningtype 0
:varnosyn 1
:varattnosyn 1
:location -1
}
:resulttype 18235
:resulttypmod -1
:resultcollid 0
:coercionformat 2
:location -1
}
A table rewrite means copying the existing, unaffected columns as-is. For the
column changing its data type, we first compute the COERCETODOMAIN node and
write the new value to the new table.
See ATRewriteTable->table_scan_getnextslot, The ExecEvalExpr below is
used to compute the new value of the changing column,
namely evaluating the COERCETODOMAIN node.
````
foreach(l, tab->newvals)
{
NewColumnValue *ex = lfirst(l);
if (ex->is_generated)
continue;
newslot->tts_values[ex->attnum - 1]
= ExecEvalExpr(ex->exprstate,
econtext,
&newslot->tts_isnull[ex->attnum - 1]);
}
````
For ALTER TABLE t ALTER COLUMN a SET DATA TYPE d1;
If we want to skip the table rewrite and do a table scan only,
We still need to use ExecEvalExpr() to evaluate the expression.
ExecEvalExpr() for COERCETODOMAIN may fail
(e.g `SELECT 1::d1`, where the value 1 cannot be cast to domain d1).
The last step of ExecInitExpr() is EEOP_DONE_RETURN, which means that when
ExecEvalExpr() evaluates an expression, if it does not
fail (No ``ereport(ERROR)`` happened), it will return a value.
see [1].
[1]
https://git.postgresql.org/cgit/postgresql.git/commit/?id=8dd7c7cd0a2605d5301266a6b67a569d6a305106
> There are other things I don’t quite understand so will give it another pass
> once it’s been rebased.
>
This attached is more bullet-proof.
It can now cope with a domain over an array of another domain, where a
table scan
should be possible (as shown below).
+CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain2 AS domain1 CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain6 AS domain2[];
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain6 USING col1;
--
jian
https://www.enterprisedb.com/
From 08c107636dd15fa7bb96226b1b4f1dd67dc683bb Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Tue, 17 Mar 2026 16:04:30 +0800
Subject: [PATCH v4 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.
This only works if the new domain type all constraint are non-volatile. and new
domain base type is binary coercible with the old column type.
[1]: https://www.postgresql.org/message-id/[email protected]
discussion: https://postgr.es/m/cacjufxfx0dupbf5+dbnf3mxcfntz1y7jpt11+tcd_fcyadh...@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/5907
---
doc/src/sgml/ref/alter_table.sgml | 5 +-
src/backend/commands/tablecmds.c | 188 ++++++++++++++++++++-
src/test/regress/expected/fast_default.out | 67 ++++++++
src/test/regress/sql/fast_default.sql | 39 +++++
4 files changed, 293 insertions(+), 6 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 8591a6b5014..5b355526522 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -1667,8 +1667,9 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
As an exception, when changing the type of an existing column,
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
+ or an unconstrained domain over the new type, or domain over new type has no
+ volatile constraint, a table rewrite is not needed.
+ However, 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/tablecmds.c b/src/backend/commands/tablecmds.c
index 67e42e5df29..958639f4fc7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -232,6 +232,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, we will evaluate the new column value in Phase 3.
+ * 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 or a
+ * container type containing a constrained domain, and all the domain's
+ * constraints are non-volatile. In case of a table rewrite, need_compute is set to true too.
*/
typedef struct NewColumnValue
{
@@ -239,6 +245,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;
/*
@@ -659,6 +666,10 @@ static void ATPrepAlterColumnType(List **wqueue,
AlterTableCmd *cmd, LOCKMODE lockmode,
AlterTableUtilityContext *context);
static bool ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno);
+static bool ATColumnChangeScanOnly(Oid inputType, Oid targetType, Node *expr);
+static bool ValidTableScanOnlyCoercionExpr(Node *expr);
+static void TypeCastTableScanOnly(Oid inputType, Oid targetType,
+ bool *scan_only);
static ObjectAddress ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
AlterTableCmd *cmd, LOCKMODE lockmode);
static void RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
@@ -6077,7 +6088,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);
/*
@@ -6272,6 +6283,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;
@@ -6500,6 +6514,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
+ * igrnore value returned by ExecEvalExpr.
+ */
+ (void) ExecEvalExpr(ex->exprstate, econtext, &isnull);
+ }
+
+ insertslot->tts_tupleDescriptor = newTupDesc;
+ }
}
/* Now check any constraints on the possibly-changed tuple */
@@ -14776,10 +14832,20 @@ ATPrepAlterColumnType(List **wqueue,
newval->attnum = attnum;
newval->expr = (Expr *) transform;
newval->is_generated = false;
+ newval->need_compute = false;
- tab->newvals = lappend(tab->newvals, newval);
- if (ATColumnChangeRequiresRewrite(transform, attnum))
+ if (!tab->rewrite && ATColumnChangeScanOnly(attTup->atttypid,
+ targettype,
+ transform))
+ {
+ newval->need_compute = true;
+ }
+ else if (ATColumnChangeRequiresRewrite(transform, attnum))
+ {
tab->rewrite |= AT_REWRITE_COLUMN_REWRITE;
+ newval->need_compute = true;
+ }
+ tab->newvals = lappend(tab->newvals, newval);
}
else if (transform)
ereport(ERROR,
@@ -14910,7 +14976,6 @@ ATPrepAlterColumnType(List **wqueue,
* rewrite in these cases:
*
* - the old type is binary coercible to the new type
- * - the new type is an unconstrained domain over the old type
* - {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
@@ -14959,6 +15024,121 @@ ATColumnChangeRequiresRewrite(Node *expr, AttrNumber varattno)
}
}
+/*
+ * Used for ALTER COLUMN SET DATA TYPE
+ *
+ * In some cases, We can just using table scan to validate the data without a
+ * table rewrite. Returns true if checking the old contents via a table scan is
+ * enough to ensure exists content is compatibility with the new data type.
+ */
+static bool
+ATColumnChangeScanOnly(Oid inputType, Oid targetType, Node *expr)
+{
+ bool scan_only = true;
+
+ if (!ValidTableScanOnlyCoercionExpr(expr))
+ return false;
+
+ TypeCastTableScanOnly(inputType,
+ targetType,
+ &scan_only);
+
+ return scan_only;
+}
+
+/*
+ * Used for ALTER COLUMN SET DATA TYPE
+ *
+ * Return false means table rewrite is required for changing the column type.
+ */
+static bool
+ValidTableScanOnlyCoercionExpr(Node *expr)
+{
+ for (;;)
+ {
+ /* only one varno, so no need to check that */
+ if (IsA(expr, Var))
+ return true;
+ else if (IsA(expr, Const))
+ return true;
+ else if (IsA(expr, CaseTestExpr))
+ return true;
+ else if (IsA(expr, RelabelType))
+ expr = (Node *) ((RelabelType *) expr)->arg;
+ else if (IsA(expr, CoerceToDomain))
+ {
+ CoerceToDomain *d = (CoerceToDomain *) expr;
+
+ expr = (Node *) d->arg;
+ }
+ else if (IsA(expr, CoerceViaIO))
+ {
+ CoerceViaIO *c = (CoerceViaIO *) expr;
+
+ expr = (Node *) c->arg;
+ }
+ else if (IsA(expr, ArrayCoerceExpr))
+ {
+ ArrayCoerceExpr *a = (ArrayCoerceExpr *) expr;
+
+ expr = (Node *) a->elemexpr;
+ }
+ else
+ return false;
+ }
+}
+
+static void
+TypeCastTableScanOnly(Oid inputType, Oid targetType,
+ bool *scan_only)
+{
+ char target_typtype = get_typtype(targetType);
+
+ /* since this function recurses, it could be driven to stack overflow */
+ check_stack_depth();
+
+ if (!(*scan_only))
+ return;
+
+ /*
+ * IsBinaryCoercible can cope with source type as domain, hence no need to
+ * check source type is domain or not
+ */
+ if (target_typtype == TYPTYPE_DOMAIN)
+ {
+ bool has_volatile = false;
+
+ (void) DomainHasConstraints(targetType, &has_volatile);
+
+ if (has_volatile)
+ {
+ *scan_only = false;
+ return;
+ }
+
+ TypeCastTableScanOnly(inputType,
+ getBaseType(targetType),
+ scan_only);
+ return;
+ }
+ else
+ {
+ Oid input_typelem = get_element_type(inputType);
+ Oid target_typelem = get_element_type(targetType);
+
+ if (OidIsValid(input_typelem) && OidIsValid(target_typelem))
+ {
+ /* recurse into array element type */
+ TypeCastTableScanOnly(input_typelem,
+ target_typelem,
+ scan_only);
+ return;
+ }
+ }
+
+ *scan_only = IsBinaryCoercible(inputType, targetType);
+}
+
/*
* ALTER COLUMN .. SET DATA TYPE
*
diff --git a/src/test/regress/expected/fast_default.out b/src/test/regress/expected/fast_default.out
index ffbc47089b1..eefb7a2a0fc 100644
--- a/src/test/regress/expected/fast_default.out
+++ b/src/test/regress/expected/fast_default.out
@@ -384,6 +384,65 @@ DROP DOMAIN domain7;
DROP DOMAIN domain8;
DROP DOMAIN domain9;
DROP FUNCTION foo(INT);
+-- Test set column data type to 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 INT8;
+CREATE DOMAIN domain4 AS domain1 CHECK(VALUE > random(min=>10, max=>10)) CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain5 AS TEXT COLLATE "POSIX" CHECK (value <> 'hello');
+CREATE DOMAIN domain6 AS domain2[];
+CREATE TABLE t22(a INT, b INT, c text COLLATE "C", col1 INT[]);
+INSERT INTO t22 VALUES(-2, -1, 'a', '{-2,null}');
+-- no table rewrite, but fail at domain constraint check
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain2[] USING col1::domain2[];
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain2[] USING col1;
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain6 USING col1;
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1;
+ERROR: value for domain domain1 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2;
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2 USING a;
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2 USING a::domain2;
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2 USING b;
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2 USING b::domain2;
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE domain5 USING 'hello' COLLATE "C";
+ERROR: value for domain domain5 violates check constraint "domain5_check"
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE domain5; -- no table rewrite
+CREATE INDEX t22_idx_c ON t22(c);
+SET client_min_messages to debug1;
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE varchar COLLATE "POSIX"; -- no table rewrite, no index rebuild
+DEBUG: verifying table "t22"
+-- no table rewrite, but index will rebuild because index collation changes
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE varchar COLLATE "C";
+DEBUG: building index "t22_idx_c" on table "t22" serially
+DEBUG: index "t22_idx_c" can safely use deduplication
+DEBUG: verifying table "t22"
+RESET client_min_messages;
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain3; -- table rewrite
+NOTICE: rewriting table t22 for reason 4
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain4; -- table rewrite, then fail
+NOTICE: rewriting table t22 for reason 4
+ERROR: value for domain domain4 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN b SET DATA TYPE domain3; -- table rewrite
+NOTICE: rewriting table t22 for reason 4
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1 USING ((a)); -- table rewrite
+NOTICE: rewriting table t22 for reason 4
+ERROR: value for domain domain1 violates check constraint "domain1_check"
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1 USING (a+0)::domain2; -- table rewrite, then fail
+NOTICE: rewriting table t22 for reason 4
+ERROR: value for domain domain2 violates check constraint "domain1_check"
+DROP TABLE t22;
+DROP DOMAIN domain1, domain2, domain3, domain4, domain5;
+ERROR: cannot drop desired object(s) because other objects depend on them
+DETAIL: type domain6 depends on type domain2[]
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
-- Fall back to full rewrite for volatile expressions
CREATE TABLE T(pk INT NOT NULL PRIMARY KEY);
INSERT INTO T VALUES (1);
@@ -986,6 +1045,14 @@ DROP TABLE has_volatile;
DROP EVENT TRIGGER has_volatile_rewrite;
DROP FUNCTION log_rewrite;
DROP SCHEMA fast_default;
+ERROR: cannot drop schema fast_default because other objects depend on it
+DETAIL: type domain1 depends on schema fast_default
+type domain2 depends on schema fast_default
+type domain3 depends on schema fast_default
+type domain4 depends on schema fast_default
+type domain5 depends on schema fast_default
+type domain6 depends on schema fast_default
+HINT: Use DROP ... CASCADE to drop the dependent objects too.
-- Leave a table with an active fast default in place, for pg_upgrade testing
set search_path = public;
create table has_fast_default(f1 int);
diff --git a/src/test/regress/sql/fast_default.sql b/src/test/regress/sql/fast_default.sql
index 8ff29cf2697..13af7728c9b 100644
--- a/src/test/regress/sql/fast_default.sql
+++ b/src/test/regress/sql/fast_default.sql
@@ -345,6 +345,45 @@ DROP DOMAIN domain8;
DROP DOMAIN domain9;
DROP FUNCTION foo(INT);
+-- Test set column data type to 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 INT8;
+CREATE DOMAIN domain4 AS domain1 CHECK(VALUE > random(min=>10, max=>10)) CHECK(VALUE > 1) NOT NULL;
+CREATE DOMAIN domain5 AS TEXT COLLATE "POSIX" CHECK (value <> 'hello');
+CREATE DOMAIN domain6 AS domain2[];
+CREATE TABLE t22(a INT, b INT, c text COLLATE "C", col1 INT[]);
+INSERT INTO t22 VALUES(-2, -1, 'a', '{-2,null}');
+
+-- no table rewrite, but fail at domain constraint check
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain2[] USING col1::domain2[];
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain2[] USING col1;
+ALTER TABLE t22 ALTER COLUMN col1 SET DATA TYPE domain6 USING col1;
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1;
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2;
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2 USING a;
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2 USING a::domain2;
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2 USING b;
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain2 USING b::domain2;
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE domain5 USING 'hello' COLLATE "C";
+
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE domain5; -- no table rewrite
+CREATE INDEX t22_idx_c ON t22(c);
+SET client_min_messages to debug1;
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE varchar COLLATE "POSIX"; -- no table rewrite, no index rebuild
+-- no table rewrite, but index will rebuild because index collation changes
+ALTER TABLE t22 ALTER COLUMN c SET DATA TYPE varchar COLLATE "C";
+RESET client_min_messages;
+
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain3; -- table rewrite
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain4; -- table rewrite, then fail
+ALTER TABLE t22 ALTER COLUMN b SET DATA TYPE domain3; -- table rewrite
+
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1 USING ((a)); -- table rewrite
+ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1 USING (a+0)::domain2; -- table rewrite, then fail
+DROP TABLE t22;
+DROP DOMAIN domain1, domain2, domain3, domain4, domain5;
+
-- Fall back to full rewrite for volatile expressions
CREATE TABLE T(pk INT NOT NULL PRIMARY KEY);
--
2.34.1