On Thu, May 14, 2026 at 5:18 PM solai v <[email protected]> wrote: > > Hi, > I tested the latest v6 patch on current master with a few different > scenarios.For the simple int to constrained-domain type change > case,the scan-only patch was used and the relfilenode stayed unchanged > before and after ALTER TABLE ,so the rewrite was avoided as expected. > I also tried USING expression cases,invalid domain values,and NULL > values.The behavior looked correct in those cases as well. > I tested with both small tables and a 1M row table. >
Thanks for testing! Actually this has a correctness issue. A domain check constraint expression can be arbitrary, so it may have side effects. `````` drop table if exists t22; drop domain if exists d1; create table t22(a int); insert into t22 values (1), (2); create or replace function domain_test() returns bool language plpgsql as $$ begin delete from t22; return true; end$$; create domain d1 as int check (domain_test()); alter table t22 alter column a set data type d1; table t22; `````` The test above shows that the V6 patch in [1] produces results that differ from master. Therefore, to skip the table rewrite when ALTER TABLE ... SET DATA TYPE changes a column to a constrained domain, we need to be conservative and ensure that: A. The domain's direct base type is a built-in data type. B. The domain's CHECK constraint contains only IMMUTABLE functions. Implementing these checks adds extra complexity, but that's not unusual — for example, we already limit virtual generated columns to built-in data types only. That said, the benefit is limited: skip table rewrite for constrained domain only covers constrained domains over built-in data types, not domain-over-domain cases. Is this worth it? Anyway, the attached patch implements this. ----------------------------------------- The above means that commens below in function ATColumnChangeRequiresRewrite is partially correct. `````` * 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. `````` [1]: https://www.postgresql.org/message-id/cacjufxewb8svirhj0zvtao99r0avc9sw0tkysmhp7v5lt7f...@mail.gmail.com -- jian https://www.enterprisedb.com/
From ababce3f5c6d767b893b6e93067f5b506cc590f3 Mon Sep 17 00:00:00 2001 From: jian he <[email protected]> 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_fcyadh...@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
