Обсуждение: [PATCH] no table rewrite when set column type to constrained domain

Поиск
Список
Период
Сортировка

[PATCH] no table rewrite when set column type to constrained domain

От
jian he
Дата:
hi.

the attached patch is to implement the $subject feature.
i was mainly intrigued by the discussion in
https://www.postgresql.org/message-id/20190226061450.GA1665944@rfd.leadboat.com

the main gotcha is struct NewColumnValue.
we do ``palloc0(sizeof(NewColumnValue));`` in ATExecAddColumn,
ATExecSetExpression, ATPrepAlterColumnType.

ATExecAddColumn:  Adding a new column with domain with constraints will cause
                  table rewrite.
ATExecSetExpression: for stored generated column will cause table rewrite, we do
                     not support domain over virtual generated columns now.
ATPrepAlterColumnType: we only do table rewriting occasionally.
see ATColumnChangeRequiresRewrite.

If table rewrite is required, then there is nothing we can do. so
we only need to focus on ATPrepAlterColumnType.
we can add a new boolean field, coerce_to_domain, to NewColumnValue. this field
is set to true only when changing an existing column's type to a constrained
domain. In such cases, a table scan is enough—no table rewrite is needed.
coerce_to_domain will set to false, if table rewrite is required.

Вложения

Re: [PATCH] no table rewrite when set column type to constrained domain

От
jian he
Дата:
On Thu, Jul 10, 2025 at 2:00 AM jian he <jian.universality@gmail.com> wrote:
>
> we can add a new boolean field, coerce_to_domain, to NewColumnValue. this field
> is set to true only when changing an existing column's type to a constrained
> domain. In such cases, a table scan is enough—no table rewrite is needed.
> coerce_to_domain will set to false, if table rewrite is required.

I realized that "coerce_to_domain" is not so good in this context.
maybe there are other scenarios, we added a NewColumnValue and we also
only need table scan.
so I changed it to scan_only.


/*
 * ....
 * If scan_only is true, it means only a table scan is required.
 * Currently, this is supported only by the ALTER COLUMN SET DATA TYPE command,
 * where the column's data type is being changed to a constrained domain.
 */
typedef struct NewColumnValue
{
    AttrNumber    attnum;         /* which column */
    Expr       *expr;                    /* expression to compute */
    ExprState  *exprstate;         /* execution state */
    bool        is_generated;       /* is it a GENERATED expression? */
    bool        scan_only;           /* table scan only */
} NewColumnValue;

Вложения

Re: [PATCH] no table rewrite when set column type to constrained domain

От
jian he
Дата:
On Tue, Aug 26, 2025 at 11:26 AM jian he <jian.universality@gmail.com> wrote:
>
> typedef struct NewColumnValue
> {
>     AttrNumber    attnum;         /* which column */
>     Expr       *expr;                    /* expression to compute */
>     ExprState  *exprstate;         /* execution state */
>     bool        is_generated;       /* is it a GENERATED expression? */
>     bool        scan_only;           /* table scan only */
> } NewColumnValue;

I changed scan_only to need_compute.

+ *
+ * If need_compute is true, we will evaluate the new column value in Phase 3.
+ * Currently, this is only used in ALTER COLUMN SET DATA TYPE
command, where the
+ * column’s data type is being changed to a constrained domain, and all the
+ * domain's constraints are non-volatile. In case table rewrite, we also set it
+ * to true.
  */
 typedef struct NewColumnValue
 {
@@ -238,6 +244,7 @@ typedef struct NewColumnValue
     Expr       *expr;            /* expression to compute */
     ExprState  *exprstate;        /* execution state */
     bool        is_generated;    /* is it a GENERATED expression? */
+    bool        need_compute;    /* compute this new expression in Phase 3 */
 } NewColumnValue;

I use domain over domain for regress tests.
I also constrained the no–table-rewrite behavior to cases where the coercion is
to a domain type and all constraints of the new domain are non-volatile.

Demo:

CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
CREATE DOMAIN domain11 AS domain1 CHECK(VALUE > 1) NOT NULL;
CREATE DOMAIN domain21 AS domain1 CHECK(VALUE > random(min=>10,
max=>10)) NOT NULL;
CREATE DOMAIN domain3 AS INT8;
CREATE TABLE t22(a INT, b INT);
INSERT INTO t22 VALUES(-2, -1);

-- no table rewrite, but fail at domain constraint check
ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain11 USING a::domain11;
-- no table rewrite, but fail at domain constraint check
ALTER TABLE t22 ALTER COLUMN b SET DATA TYPE domain11 USING b::domain11;

-- table rewrite
ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain21;
ALTER TABLE t22 ALTER COLUMN b SET DATA TYPE domain3;
ALTER TABLE t22 ALTER COLUMN a SET DATA TYPE domain1 USING (a+0)::domain1;



--
jian
https://www.enterprisedb.com

Вложения

Re: [PATCH] no table rewrite when set column type to constrained domain

От
Viktor Holmberg
Дата:
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?

There are other things I don’t quite understand so will give it another pass once it’s been rebased.

/Viktor

Re: [PATCH] no table rewrite when set column type to constrained domain

От
jian he
Дата:
On Sun, Mar 15, 2026 at 12:32 AM Viktor Holmberg <v@viktorh.net> 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
inthis 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
couldexplain 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/

Вложения

Re: [PATCH] no table rewrite when set column type to constrained domain

От
Viktor Holmberg
Дата:
On 17 Mar 2026 at 09:06 +0100, jian he <jian.universality@gmail.com>, wrote:
This attached is more bullet-proof.
My previous question was more of syntax question, and it has been addressed in the latest patch.

Questions/ comments:

The commitfest entry has two other reviewers already, but I haven’t seen any emails from them? If you are still reviewing Yogesh or Aditya, maybe let us know, or else might be best to remove them from the entry so it’s visible you’re waiting for reviewers.

-----------------------

+ 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

Why do we need to care about volatility in this patch? Wouldn’t things work with skipping the rewrite then checking the volatile checks? A rewrite doesn’t make things more deterministic right?

------------------------

+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.

+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.

Unnecessary noise in the test output?

-----------------------

Add a test like this:

CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
CREATE TABLE t_const_using(a INT);
INSERT INTO t_const_using VALUES(-2);
ALTER TABLE t_const_using ALTER COLUMN a SET DATA TYPE domain1 USING 5;
SELECT a FROM t_const_using; -- should be 5 after rewrite, not -2
 a 
----
 -2

So something is wrong with constant values.

-----------------------

Minor spelling/grammar:
- Typo: igrnore → ignore (line ~6552)
- Grammar issues in comments and the commit message ("We can just using table scan", "ensure exists content is compatibility", "the new domain type all constraint are non-volatile").

-----------------------

This is very much a non-exhaustive review as I in all honestly understand maybe like 20% of this code. But at least it’s a start.

Re: [PATCH] no table rewrite when set column type to constrained domain

От
Aditya Gollamudi
Дата:
On Tue, Mar 17, 2026 at 3:50 PM Viktor Holmberg <v@viktorh.net> wrote:
On 17 Mar 2026 at 09:06 +0100, jian he <jian.universality@gmail.com>, wrote:
This attached is more bullet-proof.
My previous question was more of syntax question, and it has been addressed in the latest patch.

Questions/ comments:

The commitfest entry has two other reviewers already, but I haven’t seen any emails from them? If you are still reviewing Yogesh or Aditya, maybe let us know, or else might be best to remove them from the entry so it’s visible you’re waiting for reviewers.

-----------------------

+ 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

Why do we need to care about volatility in this patch? Wouldn’t things work with skipping the rewrite then checking the volatile checks? A rewrite doesn’t make things more deterministic right?

------------------------

+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.

+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.

Unnecessary noise in the test output?

-----------------------

Add a test like this:

CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
CREATE TABLE t_const_using(a INT);
INSERT INTO t_const_using VALUES(-2);
ALTER TABLE t_const_using ALTER COLUMN a SET DATA TYPE domain1 USING 5;
SELECT a FROM t_const_using; -- should be 5 after rewrite, not -2
 a 
----
 -2

So something is wrong with constant values.

-----------------------

Minor spelling/grammar:
- Typo: igrnore → ignore (line ~6552)
- Grammar issues in comments and the commit message ("We can just using table scan", "ensure exists content is compatibility", "the new domain type all constraint are non-volatile").

-----------------------

This is very much a non-exhaustive review as I in all honestly understand maybe like 20% of this code. But at least it’s a start.

Hello!

Apologies for the late timing but Yogesh and I still plan on reviewing this patch.
This is one of my first patch reviews actually as part of the patch review workshop!

With that being said I would love to share some of my initial thoughts,
and to get some more clarification on the code:

This is pretty well implemented! The biggest cause for concern for me was this
temporary tuple descriptor modification. This code switches out the new tuple descriptor
for the old in order to evaluate CoerceToDomain. I’m not very familiar with this area of
the code and if this has potential ramifications but I would like more clarification
from the author on why this approach is safe/correct.

The tests, especially after this v4 are much stronger. I was also thinking it may be worth
it to add tests here for this patch on partitioned tables, to make sure the behavior holds up
across all partitions. Ensuring that for example, if we alter a column type in the parent, it is
consistent across all child partitions.

Other than that the only other things were nit picks: The naming of the new function added
“DomainHaveVolatileConstraints” was a little misleading and may lead to confusion with what
the function is actually returning, because the bool value indicating volatility actually is
determined from an output parameter. I like the idea of returning more information with
fewer function calls though.For the second commit, in one of the earlier comments, assuming the patch is accepted
should read “Previously, this was” instead of “Currently, this is”. Although I do also think 

this comment is more helpful for the reviewers than it is for understanding the code below it.

I am still waiting to hear from Yogesh, maybe he can share his thoughts here as well.

- Adi Gollamudi

Re: [PATCH] no table rewrite when set column type to constrained domain

От
jian he
Дата:
hi.

https://postgr.es/m/CAEze2Wi4M1grsR0q27etuB-jncJ6qN-1FMdRX-2PkQxcFpM3sQ@mail.gmail.com
Make me realize that ArrayCoerceExpr (one type of array cast to
another type of array) table rewrite is necessary.
For example, the following will cause a table rewrite:
+CREATE TABLE t22(a INT, b INT, c text COLLATE "C", col1 INT[]);
+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;

Now: if the new type is a constrained domain over the old type,
tablescan is enough.
ATColumnChangeRequiresRewrite works just as before.



--
jian
https://www.enterprisedb.com/

Вложения

Re: [PATCH] no table rewrite when set column type to constrained domain

От
Viktor Holmberg
Дата:
On 23 Mar 2026 at 14:57 +0100, jian he <jian.universality@gmail.com>, wrote:
hi.

https://postgr.es/m/CAEze2Wi4M1grsR0q27etuB-jncJ6qN-1FMdRX-2PkQxcFpM3sQ@mail.gmail.com
Make me realize that ArrayCoerceExpr (one type of array cast to
another type of array) table rewrite is necessary.
For example, the following will cause a table rewrite:
+CREATE TABLE t22(a INT, b INT, c text COLLATE "C", col1 INT[]);
+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;

Now: if the new type is a constrained domain over the old type,
tablescan is enough.
ATColumnChangeRequiresRewrite works just as before.

--
jian
This appears to address some of my comments but not this one?

CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
CREATE TABLE t_const_using(a INT);
INSERT INTO t_const_using VALUES(-2);
ALTER TABLE t_const_using ALTER COLUMN a SET DATA TYPE domain1 USING 5;
SELECT a FROM t_const_using; -- should be 5 after rewrite, not -2
 a
----
 -2

Re: [PATCH] no table rewrite when set column type to constrained domain

От
jian he
Дата:
On Tue, Mar 24, 2026 at 12:18 AM Viktor Holmberg <v@viktorh.net> wrote:
>
> This appears to address some of my comments but not this one?
>
> CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
> CREATE TABLE t_const_using(a INT);
> INSERT INTO t_const_using VALUES(-2);
> ALTER TABLE t_const_using ALTER COLUMN a SET DATA TYPE domain1 USING 5;
> SELECT a FROM t_const_using; -- should be 5 after rewrite, not -2
>  a
> ----
>  -2

Sure, these tests are added to v6.

--
jian
https://www.enterprisedb.com/

Вложения

Re: [PATCH] no table rewrite when set column type to constrained domain

От
Viktor Holmberg
Дата:
On 24 Mar 2026 at 02:17 +0100, jian he <jian.universality@gmail.com>, wrote:
On Tue, Mar 24, 2026 at 12:18 AM Viktor Holmberg <v@viktorh.net> wrote:

This appears to address some of my comments but not this one?

CREATE DOMAIN domain1 AS INT CHECK(VALUE > 1) NOT NULL;
CREATE TABLE t_const_using(a INT);
INSERT INTO t_const_using VALUES(-2);
ALTER TABLE t_const_using ALTER COLUMN a SET DATA TYPE domain1 USING 5;
SELECT a FROM t_const_using; -- should be 5 after rewrite, not -2
a
----
-2

Sure, these tests are added to v6.

--
jian
https://www.enterprisedb.com/
@@ -6077,7 +6084,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);

I’m not 100% sure, but I think when you add an unconstraint domain:
CREATE DOMAIN mydom AS int; -- no CHECK, no NOT NULL
 CREATE TABLE t (a int);
 ALTER TABLE t ALTER COLUMN a TYPE mydom;

The tab->newvals check makes it so that ATRewriteTable is run, even though it’s not needed.

---
/*
 * ExecEvalExprNoReturn cannot be used here because
 * the expression was compiled via ExecInitExpr.
 */
 (void) ExecEvalExpr(ex->exprstate, econtext, &isnull);

I still don’t understand this comment at all. Not saying it’s wrong, but not sure if it’s right or not. Perhaps its clear for those that are more in the know so maybe if you can explain in an email it’s be enough.

---
Nit:
-- Test chaning  column data type to constrained domain

 "chaning" → "changing", and there's a double space.