agora inbox for pgsql-hackers@postgresql.org
help / color / mirror / Atom feed[PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
11+ messages / 5 participants
[nested] [flat]
* [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-28 13:29 Nitin Motiani <nitinmotiani@google.com>
0 siblings, 2 replies; 11+ messages in thread
From: Nitin Motiani @ 2026-09-28 13:29 UTC (permalink / raw)
To: pgsql-hackers
Hi,
The following bug was reported in [1]
```
CREATE TYPE rt AS (i int);
CREATE DOMAIN dt AS int CHECK ((ROW(value)::rt).i > 0);
ALTER TYPE rt ALTER ATTRIBUTE i TYPE bigint;
triggers an internal
ERROR: XX000: could not identify relation associated with constraint 16390
LOCATION: ATPostAlterTypeCleanup, tablecmds.c:16147
Reproduced starting from af20e2d72.
```
I am not sure how common this scenario is but I investigated the
history of the commit af20e2d72. LLM pointed me to [2] from 2017.
The thread mentions a similar case in the email but it isn't covered
in the test cases.
```
regression=# create type comptype as (r float8, i float8);
CREATE TYPE
regression=# create domain silly as float8 check
((row(value,0)::comptype).r > 0);
CREATE DOMAIN
regression=# alter type comptype alter attribute r type varchar;
ERROR: cache lookup failed for relation 0
```
Therefore I am attaching a patch file with a proposed fix. The issue
stems from the fact that for a domain constraint, we currently look
for the domain's base type and the corresponding relid. But if the
domain is over a primitive type like int, there is no relid. And
therefore it fails.
My understanding of code is that this relid is only being used in
ATPostAlterTypeParse to unqueue the entry corresponding to the type
being altered.
So in this patch instead of getting the relid from the domain, we use
the relid of the type being altered.
I tested changing int to bigint and text to ensure it passes in the
first case and fails in the second.
Please take a look and let me know what you think.
[1] https://www.postgresql.org/message-id/flat/19724-58468097b5b17d10%40postgresql.org
[2] https://www.postgresql.org/message-id/flat/30656.1509128130%40sss.pgh.pa.us
Regards & Thanks,
Nitin Motiani
Google
Attachments:
[application/x-patch] v1-0001-Fix-ALTER-TYPE-.-ALTER-ATTRIBUTE-on-types-used-in.patch (5.7K, ../../CAH5HC94+4teDZvuWkBiAikcK1pM2DH9W6iCNDoGpHxUdPGTNPw@mail.gmail.com/2-v1-0001-Fix-ALTER-TYPE-.-ALTER-ATTRIBUTE-on-types-used-in.patch)
download | inline diff:
From e435c2e31abe47935e74a3d0f547ba5476b5c1c7 Mon Sep 17 00:00:00 2001
From: Nitin Motiani <nitinmotiani@google.com>
Date: Mon, 28 Sep 2026 12:49:33 +0000
Subject: [PATCH v1] Fix ALTER TYPE ... ALTER ATTRIBUTE on types used in domain
constraints.
Commit af20e2d72 updated ALTER TABLE / TYPE to rebuild domain constraints
when an attribute of a composite type is altered. However, it assumed that
the domain's base type was always the composite type being altered, calling
get_typ_typrelid(getBaseType(con->contypid)). If the domain was defined
over a scalar type (such as int or float8) whose CHECK expression referenced
the composite type, get_typ_typrelid() returned InvalidOid, triggering
an internal "could not identify relation associated with constraint" error.
Fix by attaching the deferred domain constraint rebuild command to the
table being altered (tab->relid) rather than attempting to derive a relation
OID from the domain's base type. Domains do not have pg_class relations of
their own, and the rebuild command (AlterDomainStmt) is self-contained.
Reported-by: Alexander Lakhin
Bug: #19724
---
src/backend/commands/tablecmds.c | 10 +++---
src/test/regress/expected/domain.out | 50 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 33 ++++++++++++++++++
3 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0274d892f2e..c8bc193a2ab 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16141,10 +16141,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
relid = con->conrelid;
else
{
- /* must be a domain constraint */
- relid = get_typ_typrelid(getBaseType(con->contypid));
- if (!OidIsValid(relid))
- elog(ERROR, "could not identify relation associated with constraint %u", oldId);
+ /*
+ * Must be a domain constraint. Domains don't have their own
+ * relations, so attach the rebuild step to the table being
+ * altered.
+ */
+ relid = tab->relid;
}
confrelid = con->confrelid;
conislocal = con->conislocal;
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..de60a90c045 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -432,6 +432,56 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
NOTICE: drop cascades to type dcomptype
+-- regression tests for bug #19724
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+ERROR: operator does not exist: text > integer
+DETAIL: No operator of that name accepts the given argument types.
+HINT: You might need to add explicit type casts.
+alter type rt alter attribute i type bigint;
+select 1::dt;
+ dt
+----
+ 1
+(1 row)
+
+select (-1)::dt; -- fail
+ERROR: value for domain dt violates check constraint "dt_check"
+drop domain dt;
+drop type rt cascade;
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+ silly
+-------
+ 1
+(1 row)
+
+select (-1.0)::silly; -- fail
+ERROR: value for domain silly violates check constraint "silly_check"
+drop domain silly;
+drop type comptype cascade;
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+ dt_multi
+----------
+ 1
+(1 row)
+
+select (-1)::dt_multi; -- fail
+ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..1240f9422bd 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -219,6 +219,39 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
+-- regression tests for bug #19724
+
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+alter type rt alter attribute i type bigint;
+select 1::dt;
+select (-1)::dt; -- fail
+drop domain dt;
+drop type rt cascade;
+
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+select (-1.0)::silly; -- fail
+drop domain silly;
+drop type comptype cascade;
+
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+select (-1)::dt_multi; -- fail
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
+
-- Test domains over arrays of composite
--
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-28 14:31 Ayush Tiwari <ayushtiwari.slg01@gmail.com>
parent: Nitin Motiani <nitinmotiani@google.com>
1 sibling, 1 reply; 11+ messages in thread
From: Ayush Tiwari @ 2026-09-28 14:31 UTC (permalink / raw)
To: Nitin Motiani <nitinmotiani@google.com>; +Cc: pgsql-hackers
Hi,
On Mon, 28 Sept 2026 at 18:59, Nitin Motiani <nitinmotiani@google.com>
wrote:
> The following bug was reported in [1]
>
> ```
> CREATE TYPE rt AS (i int);
> CREATE DOMAIN dt AS int CHECK ((ROW(value)::rt).i > 0);
> ALTER TYPE rt ALTER ATTRIBUTE i TYPE bigint;
>
>
>
> triggers an internal
> ERROR: XX000: could not identify relation associated with constraint
16390
> LOCATION: ATPostAlterTypeCleanup, tablecmds.c:16147
>
> Reproduced starting from af20e2d72.
>
> ```
>
> I am not sure how common this scenario is but I investigated the
> history of the commit af20e2d72. LLM pointed me to [2] from 2017.
>
> The thread mentions a similar case in the email but it isn't covered
> in the test cases.
>
> ```
> regression=# create type comptype as (r float8, i float8);
> CREATE TYPE
> regression=# create domain silly as float8 check
> ((row(value,0)::comptype).r > 0);
> CREATE DOMAIN
> regression=# alter type comptype alter attribute r type varchar;
> ERROR: cache lookup failed for relation 0
> ```
>
> Therefore I am attaching a patch file with a proposed fix. The issue
> stems from the fact that for a domain constraint, we currently look
> for the domain's base type and the corresponding relid. But if the
> domain is over a primitive type like int, there is no relid. And
> therefore it fails.
>
> My understanding of code is that this relid is only being used in
> ATPostAlterTypeParse to unqueue the entry corresponding to the type
> being altered.
>
> So in this patch instead of getting the relid from the domain, we use
> the relid of the type being altered.
>
> I tested changing int to bigint and text to ensure it passes in the
> first case and fails in the second.
>
> Please take a look and let me know what you think.
Thanks for working on this. Switching to tab->relid looks right to me,
since a domain constraint doesn't belong to any relation anyway.
I think there's still a problem when a table that uses the domain gets
rewritten by the same ALTER, though. The re-added constraint is
validated right away in phase 2, before phase 3 has rewritten those
tables, so their old rows are read with the new tuple descriptor. With
the patch this crashes for me:
create table dtab (i int);
create domain dtext as text check ((row(1)::dtab).i > 0 and md5(value) <>
'');
create table dtab_child (t dtext) inherits (dtab);
insert into dtab_child values (1, repeat('x', 200));
alter table dtab alter column i type bigint;
On master it just fails with the elog. (FWIW master, and 14 too as far
as I checked, can already crash like this with a domain over the
parent's rowtype stored in the child, so it's not entirely new.)
Maybe the constraint could be re-added as NOT VALID in phase 2, and then
validated once phase 3 is done, e.g. with AlterDomainValidateConstraint()
next to the FK checks at the end of ATRewriteTables()?
Also, I believe the test cases can be trimmed.
For the tests, all three cases take the same path and none of them
stores the domain in a table. One test for the reported case plus one
like the above might be enough.
Regards,
Ayush
[1]
https://www.postgresql.org/message-id/flat/19724-58468097b5b17d10%40postgresql.org
[2]
https://www.postgresql.org/message-id/flat/30656.1509128130%40sss.pgh.pa.us
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-28 16:02 Rahul Yadav <rahul@rhyadav.com>
parent: Nitin Motiani <nitinmotiani@google.com>
1 sibling, 1 reply; 11+ messages in thread
From: Rahul Yadav @ 2026-09-28 16:02 UTC (permalink / raw)
To: nitinmotiani@google.com; +Cc: pgsql-hackers@lists.postgresql.org
Hi Nitin,
I reviewed and tested v1. The fix looks right to me. For a domain
constraint, the relation OID in ATPostAlterTypeCleanup() only decides
whether another relation gets locked and which work-queue entry gets
the AT_ReAddDomainConstraint command. The AlterDomainStmt names the
domain itself, so using tab->relid is fine.
Testing (macOS arm64, meson debug build with assertions, master at
ad36e3608c plus v1): v1 applies cleanly with git am, and the
regression tests pass. I also ran these cases on master and on the
patched build. Master fails on all of them with "could not identify
relation associated with constraint". With v1 the type change goes
through and the constraint is rebuilt:
- ALTER TABLE ... ALTER COLUMN ... TYPE on a table whose row type is
used in the check of a domain over int
- a domain over int[], and a domain over a domain over int
- a typed table, with ALTER TYPE ... CASCADE
- one domain check that uses both a parent's and a child's row type,
with ALTER TABLE on the parent
- a table column of the domain type with data in it (the constraint
is revalidated)
- a comment on the constraint (kept) and a NOT VALID constraint
(stays NOT VALID)
Where I checked, the rebuilt constraint still rejects bad values.
Comments:
1. The bug isn't specific to ALTER TYPE. ALTER TABLE ... ALTER
COLUMN ... TYPE hits it too, as in the first case above. I'd add
a test for that and mention it in the commit message.
2. The commit message says the domain is "defined over a scalar
type". Arrays and domains over domains are affected too, so "a
domain whose base type isn't composite" would be more accurate.
3. In the tests, I'd describe the float8 case instead of referring to
"Tom Lane's 2017 email".
4. One side effect: for a domain over a different composite type, as
in
CREATE DOMAIN dd AS r2 CHECK ((ROW((VALUE).b)::r1).a > 0);
ALTER TYPE r1 ALTER ATTRIBUTE a TYPE bigint;
master takes an AccessExclusiveLock on r2 and queues the rebuild
there, while v1 does neither. I think that's fine, since the
rebuilt constraint belongs to the domain and doesn't touch r2.
5. af20e2d72 went into v11, so every supported branch has this bug.
v1, tests included, applies cleanly to REL_14_STABLE through
REL_19_STABLE, so +1 for back-patching.
I couldn't find this in the open commitfest (PG20-3). Could you add
it? I'm happy to be listed as a reviewer.
Regards,
Rahul Yadav
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-28 17:05 Nitin Motiani <nitinmotiani@google.com>
parent: Rahul Yadav <rahul@rhyadav.com>
0 siblings, 0 replies; 11+ messages in thread
From: Nitin Motiani @ 2026-09-28 17:05 UTC (permalink / raw)
To: Rahul Yadav <rahul@rhyadav.com>; +Cc: pgsql-hackers@lists.postgresql.org
Thanks Rahul.
I've created the commitfest entry here
https://commitfest.postgresql.org/patch/7356/.
Regards,
Nitin Motiani
Google
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-28 18:45 Matheus Alcantara <matheusssilv97@gmail.com>
parent: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
0 siblings, 2 replies; 11+ messages in thread
From: Matheus Alcantara @ 2026-09-28 18:45 UTC (permalink / raw)
To: Ayush Tiwari <ayushtiwari.slg01@gmail.com>; Nitin Motiani <nitinmotiani@google.com>; +Cc: pgsql-hackers
Hi,
I've reviewed and tested Nitin's v1 as well, and it works as expected.
The reported case and the variants I tried go through, and the rebuilt
constraint is still enforced. For a domain constraint the relid only
decides which relation gets locked and which work queue entry gets the
AT_ReAddDomainConstraint command, so tab->relid seems right to me.
On Mon Sep 28, 2026 at 11:31 AM -03, Ayush Tiwari wrote:
> Thanks for working on this. Switching to tab->relid looks right to me,
> since a domain constraint doesn't belong to any relation anyway.
>
> I think there's still a problem when a table that uses the domain gets
> rewritten by the same ALTER, though. The re-added constraint is
> validated right away in phase 2, before phase 3 has rewritten those
> tables, so their old rows are read with the new tuple descriptor. With
> the patch this crashes for me:
>
Yes, I found the same problem while testing v1, plus a second one.
Both date back to af20e2d72 and can already be hit on master with a
domain over a composite type, but with v1 they are reachable for any
domain whose check expression references a composite type.
1. Re-added domain constraints are validated before the rewrite.
As you describe, AT_ReAddDomainConstraint calls
AlterDomainAddConstraint(), which validates the constraint immediately,
while tables using the domain may still be pending a rewrite. A simple
way to see it:
create function show(int) returns bool language plpgsql as
$$ begin raise notice 'domain check sees value %', $1; return true; end $$;
create table t (c int);
create domain dt as int check (show(value) and (null::t).c is null);
alter table t add column d dt;
insert into t values (1, 5), (2, 7);
alter table t alter column c type bigint;
NOTICE: domain check sees value 0
NOTICE: domain check sees value 700
Depending on the data this gives garbage values, spurious "contains
values that violate the new constraint" errors, errors like "type with
OID 4294967295 does not exist" (domain over composite, on master), or
a crash like in your example.
2. Rebuilding the constraint requires ownership of the domain.
AlterDomainAddConstraint() calls checkDomainOwner(), so if another
user's domain has a constraint that depends on your type, you can't
alter your own type:
-- as alice
create type rt as (i int);
-- as bob
create domain dt as int check ((row(value)::rt).i > 0);
-- as alice
alter type rt alter attribute i type bigint;
ERROR: must be owner of type dt
Since types grant USAGE to PUBLIC by default, any user can create such
a dependency. The matching drop is done without permission checks, and
rebuilding another user's table CHECK constraint doesn't check
ownership either. A related case: if the constraint has a comment, it
is restored via CommentObject(), which also requires ownership. That
affects table constraints too, even without domains:
-- as bob
create table tt (x int constraint k check ((row(x)::rt).i > 0));
comment on constraint k on tt is 'hi';
-- as alice
alter type rt alter attribute i type bigint;
ERROR: must be owner of relation tt
> create table dtab (i int);
> create domain dtext as text check ((row(1)::dtab).i > 0 and md5(value) <>
> '');
> create table dtab_child (t dtext) inherits (dtab);
> insert into dtab_child values (1, repeat('x', 200));
> alter table dtab alter column i type bigint;
>
> On master it just fails with the elog. (FWIW master, and 14 too as far
> as I checked, can already crash like this with a domain over the
> parent's rowtype stored in the child, so it's not entirely new.)
>
> Maybe the constraint could be re-added as NOT VALID in phase 2, and then
> validated once phase 3 is done, e.g. with AlterDomainValidateConstraint()
> next to the FK checks at the end of ATRewriteTables()?
>
I considered that, but I think it's simpler to skip validation when
re-adding, remember the new constraint's OID in the work queue entry,
and call validateDomainCheckConstraint() on it once all tables have
been rewritten. My reasons:
- AlterDomainValidateConstraint() also calls checkDomainOwner(), so it
has issue 2 as well. Unlike AlterDomainAddConstraint() it has no
is_readd flag, so we'd need to add a new parameter to it too.
- It looks up the domain and the constraint by name. Using the OID
avoids resolving the names again in phase 3; ATPostAlterTypeCleanup()
already works from OIDs for similar reasons.
- We'd still need to remember which constraints were valid before the
rebuild, so that a NOT VALID constraint isn't validated. That's the
same bookkeeping as queuing the OID.
- The FK validation loop skips relations without storage. For ALTER TYPE
on a standalone composite type the only work queue entry is the type
itself, so validating there would silently skip it. For example, with
a stored value of 40000, changing an attribute from int to smallint
must still fail with "smallint out of range". The new loop on the
attached 0002 patch runs over the whole work queue for this reason.
One more thing I noticed, not addressed by these patches: if a domain
check uses ROW(value)::t and a column of the domain is later added to
t, the deparsed constraint becomes ROW(VALUE, NULL)::t. Re-parsing
that coerces the NULL to the domain itself, so the rebuilt constraint
refers to its own domain and fails with "stack depth limit exceeded".
The same happens with a hand-written ALTER DOMAIN ... ADD CONSTRAINT
using ROW(value, null)::t, so it's not specific to this code path,
which is why the tests use (null::t).c instead.
Attached are:
- v2-0001: Nitin's v1, unchanged.
- v2-0002: validate re-added domain constraints after the rewrites
(issue 1).
- v2-0003: don't require ownership when rebuilding constraints and their
comments (issue 2).
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
From eb6112c7e81eb2de0027b016ae27e08abf86447a Mon Sep 17 00:00:00 2001
From: Nitin Motiani <nitinmotiani@google.com>
Date: Mon, 28 Sep 2026 12:49:33 +0000
Subject: [PATCH v2 1/3] Fix ALTER TYPE ... ALTER ATTRIBUTE on types used in
domain constraints.
Commit af20e2d72 updated ALTER TABLE / TYPE to rebuild domain constraints
when an attribute of a composite type is altered. However, it assumed that
the domain's base type was always the composite type being altered, calling
get_typ_typrelid(getBaseType(con->contypid)). If the domain was defined
over a scalar type (such as int or float8) whose CHECK expression referenced
the composite type, get_typ_typrelid() returned InvalidOid, triggering
an internal "could not identify relation associated with constraint" error.
Fix by attaching the deferred domain constraint rebuild command to the
table being altered (tab->relid) rather than attempting to derive a relation
OID from the domain's base type. Domains do not have pg_class relations of
their own, and the rebuild command (AlterDomainStmt) is self-contained.
Reported-by: Alexander Lakhin
Bug: #19724
---
src/backend/commands/tablecmds.c | 10 +++---
src/test/regress/expected/domain.out | 50 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 33 ++++++++++++++++++
3 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0274d892f2e..c8bc193a2ab 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16141,10 +16141,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
relid = con->conrelid;
else
{
- /* must be a domain constraint */
- relid = get_typ_typrelid(getBaseType(con->contypid));
- if (!OidIsValid(relid))
- elog(ERROR, "could not identify relation associated with constraint %u", oldId);
+ /*
+ * Must be a domain constraint. Domains don't have their own
+ * relations, so attach the rebuild step to the table being
+ * altered.
+ */
+ relid = tab->relid;
}
confrelid = con->confrelid;
conislocal = con->conislocal;
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..de60a90c045 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -432,6 +432,56 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
NOTICE: drop cascades to type dcomptype
+-- regression tests for bug #19724
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+ERROR: operator does not exist: text > integer
+DETAIL: No operator of that name accepts the given argument types.
+HINT: You might need to add explicit type casts.
+alter type rt alter attribute i type bigint;
+select 1::dt;
+ dt
+----
+ 1
+(1 row)
+
+select (-1)::dt; -- fail
+ERROR: value for domain dt violates check constraint "dt_check"
+drop domain dt;
+drop type rt cascade;
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+ silly
+-------
+ 1
+(1 row)
+
+select (-1.0)::silly; -- fail
+ERROR: value for domain silly violates check constraint "silly_check"
+drop domain silly;
+drop type comptype cascade;
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+ dt_multi
+----------
+ 1
+(1 row)
+
+select (-1)::dt_multi; -- fail
+ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..1240f9422bd 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -219,6 +219,39 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
+-- regression tests for bug #19724
+
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+alter type rt alter attribute i type bigint;
+select 1::dt;
+select (-1)::dt; -- fail
+drop domain dt;
+drop type rt cascade;
+
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+select (-1.0)::silly; -- fail
+drop domain silly;
+drop type comptype cascade;
+
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+select (-1)::dt_multi; -- fail
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
From 99f8cbb9144ad3a6deb5ed110b3426efa278f522 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:05:36 -0300
Subject: [PATCH v2 2/3] Validate re-added domain constraints after ALTER TABLE
rewrites
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a domain CHECK constraint whose expression depends
on the altered column, the constraint was re-added through
AlterDomainAddConstraint(), which validates it immediately against all
columns of the domain. That happens during Phase 2, before Phase 3 has
rewritten the affected tables, so any table that is pending a rewrite
and has a column of the domain was scanned using its new tuple
descriptor over its old heap. This could produce garbage values,
spurious "contains values that violate the new constraint" errors, or
worse, e.g. "type with OID 4294967295 does not exist" when the domain is
over a composite type.
Fix by skipping validation in AlterDomainAddConstraint() when re-adding
a constraint, and instead having ATExecCmd() remember the rebuilt
constraint so that ATRewriteTables() validates it once all tables have
been rewritten. Constraints that were NOT VALID are not validated, as
before.
This problem dates back to af20e2d72, which added rebuilding of domain
constraints, but was previously only reachable with domains over
composite types, since other domains hit the "could not identify
relation associated with constraint" error instead.
---
src/backend/commands/tablecmds.c | 56 ++++++++++++++--
src/backend/commands/typecmds.c | 11 ++--
src/include/commands/typecmds.h | 1 +
src/test/regress/expected/domain.out | 99 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 63 ++++++++++++++++++
5 files changed, 221 insertions(+), 9 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c8bc193a2ab..10c7a3a5f8e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -198,6 +198,8 @@ typedef struct AlteredTableInfo
bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
char newrelpersistence; /* if above is true */
Expr *partition_constraint; /* for attach partition validation */
+ /* OIDs of re-added domain CHECK constraints to validate in Phase 3 */
+ List *domain_constraints;
/* true, if validating default due to some other attach/detach */
bool validate_default;
/* Objects to rebuild after completing ALTER TYPE operations */
@@ -5535,11 +5537,25 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
case AT_ReAddDomainConstraint: /* Re-add pre-existing domain check
* constraint */
- address =
- AlterDomainAddConstraint(((AlterDomainStmt *) cmd->def)->typeName,
- ((AlterDomainStmt *) cmd->def)->def,
- NULL, true);
- break;
+ {
+ AlterDomainStmt *stmt = (AlterDomainStmt *) cmd->def;
+ Constraint *con = castNode(Constraint, stmt->def);
+ ObjectAddress constrAddr;
+
+ address = AlterDomainAddConstraint(stmt->typeName, stmt->def,
+ &constrAddr, true);
+
+ /*
+ * AlterDomainAddConstraint doesn't validate re-added
+ * constraints, since tables using the domain may not have
+ * been rewritten yet. Tell Phase 3 to do it.
+ */
+ if (!con->skip_validation)
+ tab->domain_constraints =
+ lappend_oid(tab->domain_constraints,
+ constrAddr.objectId);
+ break;
+ }
case AT_ReAddComment: /* Re-add existing comment */
address = CommentObject((CommentStmt *) cmd->def);
break;
@@ -6160,6 +6176,36 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
table_close(rel, NoLock);
}
+ /*
+ * Validate re-added domain CHECK constraints. This must wait until all
+ * tables have been rewritten, since any of them might contain columns of
+ * the domain. Don't skip relations without storage since the work queue
+ * entry might be for a standalone composite type.
+ */
+ foreach(ltab, *wqueue)
+ {
+ AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
+
+ foreach_oid(conoid, tab->domain_constraints)
+ {
+ HeapTuple tup;
+ Form_pg_constraint con;
+ Datum conbin;
+
+ tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(conoid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for constraint %u", conoid);
+ con = (Form_pg_constraint) GETSTRUCT(tup);
+
+ conbin = SysCacheGetAttrNotNull(CONSTROID, tup,
+ Anum_pg_constraint_conbin);
+ validateDomainCheckConstraint(con->contypid,
+ TextDatumGetCString(conbin));
+
+ ReleaseSysCache(tup);
+ }
+ }
+
/* Finally, run any afterStmts that were queued up */
foreach(ltab, *wqueue)
{
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 99a0ae2228e..d0349079a1e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -128,7 +128,6 @@ static Oid findTypeSubscriptingFunction(List *procname, Oid typeOid);
static Oid findRangeSubOpclass(List *opcname, Oid subtype);
static Oid findRangeCanonicalFunction(List *procname, Oid typeOid);
static Oid findRangeSubtypeDiffFunction(List *procname, Oid subtype);
-static void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
static void validateDomainNotNullConstraint(Oid domainoid);
static List *get_rels_with_domain(Oid domainOid, LOCKMODE lockmode);
static void checkEnumOwner(HeapTuple tup);
@@ -3029,13 +3028,17 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
constr, NameStr(typTup->typname), constrAddr,
is_readd);
-
/*
* If requested to validate the constraint, test all values stored in
* the attributes based on the domain the constraint is being added
* to.
+ *
+ * When re-adding a constraint during ALTER TABLE, the tables using
+ * the domain might not have been rewritten to match their new
+ * catalog definitions yet, so the caller must do the validation after
+ * its rewrite phase instead.
*/
- if (!constr->skip_validation)
+ if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
/*
@@ -3249,7 +3252,7 @@ validateDomainNotNullConstraint(Oid domainoid)
* Verify that all columns currently using the domain satisfy the given check
* constraint expression.
*/
-static void
+void
validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
{
Expr *expr = (Expr *) stringToNode(ccbin);
diff --git a/src/include/commands/typecmds.h b/src/include/commands/typecmds.h
index 2112b4addd2..a067651f6f9 100644
--- a/src/include/commands/typecmds.h
+++ b/src/include/commands/typecmds.h
@@ -38,6 +38,7 @@ extern ObjectAddress AlterDomainAddConstraint(List *names, Node *newConstraint,
ObjectAddress *constrAddr,
bool is_readd);
extern ObjectAddress AlterDomainValidateConstraint(List *names, const char *constrName);
+extern void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
extern ObjectAddress AlterDomainDropConstraint(List *names, const char *constrName,
DropBehavior behavior, bool missing_ok);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index de60a90c045..14f2c928700 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -482,6 +482,105 @@ ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
drop domain dt_multi;
drop type r1 cascade;
drop type r2 cascade;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+ convalidated
+--------------
+ t
+(1 row)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+-----
+ 1 | (5)
+ 2 | (7)
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_ch
+drop table domrw_p cascade;
+NOTICE: drop cascades to table domrw_ch
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+ERROR: smallint out of range
+drop table domrw_u;
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+ pg_get_constraintdef | convalidated
+----------------------------------------------------------------------+--------------
+ CHECK (((ROW((VALUE)::smallint)::domrw_rt).i IS NOT NULL)) NOT VALID | f
+(1 row)
+
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 1240f9422bd..b6e452d6ebb 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -252,6 +252,69 @@ drop domain dt_multi;
drop type r1 cascade;
drop type r2 cascade;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+drop domain domrw_dt cascade;
+drop table domrw_p cascade;
+
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+drop table domrw_u;
+
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
From bf78ab6a32c5e27572588cfa5867ed4e6f2049b6 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:18:56 -0300
Subject: [PATCH v2 3/3] Don't require ownership when rebuilding constraints in
ALTER TABLE
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a constraint that depends on the altered column,
it drops the constraint and re-creates it from its saved definition.
For domain CHECK constraints the re-creation goes through
AlterDomainAddConstraint(), which calls checkDomainOwner(), and any
comment on a table or domain constraint is restored with
CommentObject(), which requires ownership of the constraint's table or
domain. So a user altering a type or table they own would fail with
"must be owner of type ..." or "must be owner of relation ..." if
another user's domain or table has a constraint that depends on it.
Since types grant USAGE to PUBLIC by default, any user could create
such a dependency and block the owner from altering their own type.
These checks don't make sense here: the user isn't choosing to add a
constraint or comment, just restoring ones that already existed, and
the matching drop is already done without any permission checks.
Rebuilding a table constraint without a comment also doesn't check
ownership.
Fix by skipping the ownership check in AlterDomainAddConstraint() when
is_readd is set, as we already do for the USAGE check on types used
by the expression, and by restoring comments directly with
CreateComments() instead of CommentObject().
The domain part of this dates back to af20e2d72, which added
rebuilding of domain constraints.
---
src/backend/commands/tablecmds.c | 22 +++++++++++++++-
src/backend/commands/typecmds.c | 18 +++++++++----
src/test/regress/expected/domain.out | 38 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 31 +++++++++++++++++++++++
4 files changed, 103 insertions(+), 6 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 10c7a3a5f8e..a6c1d43cc0e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -5557,7 +5557,27 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
}
case AT_ReAddComment: /* Re-add existing comment */
- address = CommentObject((CommentStmt *) cmd->def);
+ {
+ CommentStmt *stmt = (CommentStmt *) cmd->def;
+ Relation comrel;
+
+ /*
+ * Don't use CommentObject(), since that requires ownership of
+ * the constraint's table or domain, which the user altering a
+ * column the constraint depends on need not have. We're just
+ * restoring a comment that already existed.
+ */
+ Assert(stmt->objtype == OBJECT_TABCONSTRAINT ||
+ stmt->objtype == OBJECT_DOMCONSTRAINT);
+ address = get_object_address(stmt->objtype, stmt->object,
+ &comrel,
+ ShareUpdateExclusiveLock,
+ false);
+ CreateComments(address.objectId, address.classId,
+ address.objectSubId, stmt->comment);
+ if (comrel != NULL)
+ relation_close(comrel, NoLock);
+ }
break;
case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
address = ATExecAddIndexConstraint(tab, rel, (IndexStmt *) cmd->def,
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index d0349079a1e..69cdfd87a79 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3004,8 +3004,16 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
elog(ERROR, "cache lookup failed for type %u", domainoid);
typTup = (Form_pg_type) GETSTRUCT(tup);
- /* Check it's a domain and check user has permission for ALTER DOMAIN */
- checkDomainOwner(tup);
+ /*
+ * Check it's a domain and check user has permission for ALTER DOMAIN.
+ * When re-adding a constraint during ALTER TABLE, skip the permission
+ * check since the constraint already existed, and the user altering a
+ * column it depends on need not own the domain.
+ */
+ if (is_readd)
+ Assert(typTup->typtype == TYPTYPE_DOMAIN);
+ else
+ checkDomainOwner(tup);
if (!IsA(newConstraint, Constraint))
elog(ERROR, "unrecognized node type: %d",
@@ -3034,9 +3042,9 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
* to.
*
* When re-adding a constraint during ALTER TABLE, the tables using
- * the domain might not have been rewritten to match their new
- * catalog definitions yet, so the caller must do the validation after
- * its rewrite phase instead.
+ * the domain might not have been rewritten to match their new catalog
+ * definitions yet, so the caller must do the validation after its
+ * rewrite phase instead.
*/
if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 14f2c928700..e49c5dacab3 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -581,6 +581,44 @@ select pg_get_constraintdef(oid), convalidated from pg_constraint
drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+ conname | pg_get_constraintdef | obj_description
+-----------------+--------------------------------------------------+-----------------------
+ domrw_dt1_check | CHECK (((ROW((VALUE)::bigint)::domrw_rt).i > 0)) | domain over int
+ domrw_dt2_check | CHECK (((VALUE).i > 0)) | domain over composite
+ domrw_t_check | CHECK (((ROW((x)::bigint)::domrw_rt).i > 0)) | table constraint
+(3 rows)
+
+select (-1)::domrw_dt1; -- fail
+ERROR: value for domain domrw_dt1 violates check constraint "domrw_dt1_check"
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b6e452d6ebb..e914b6913ee 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -315,6 +315,37 @@ drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+select (-1)::domrw_dt1; -- fail
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v2-0001-Fix-ALTER-TYPE-.-ALTER-ATTRIBUTE-on-types-used-in.patch (5.7K, ../../DLR67P6TO2C8.3SL4J7XR66QBJ@gmail.com/2-v2-0001-Fix-ALTER-TYPE-.-ALTER-ATTRIBUTE-on-types-used-in.patch)
download | inline diff:
From eb6112c7e81eb2de0027b016ae27e08abf86447a Mon Sep 17 00:00:00 2001
From: Nitin Motiani <nitinmotiani@google.com>
Date: Mon, 28 Sep 2026 12:49:33 +0000
Subject: [PATCH v2 1/3] Fix ALTER TYPE ... ALTER ATTRIBUTE on types used in
domain constraints.
Commit af20e2d72 updated ALTER TABLE / TYPE to rebuild domain constraints
when an attribute of a composite type is altered. However, it assumed that
the domain's base type was always the composite type being altered, calling
get_typ_typrelid(getBaseType(con->contypid)). If the domain was defined
over a scalar type (such as int or float8) whose CHECK expression referenced
the composite type, get_typ_typrelid() returned InvalidOid, triggering
an internal "could not identify relation associated with constraint" error.
Fix by attaching the deferred domain constraint rebuild command to the
table being altered (tab->relid) rather than attempting to derive a relation
OID from the domain's base type. Domains do not have pg_class relations of
their own, and the rebuild command (AlterDomainStmt) is self-contained.
Reported-by: Alexander Lakhin
Bug: #19724
---
src/backend/commands/tablecmds.c | 10 +++---
src/test/regress/expected/domain.out | 50 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 33 ++++++++++++++++++
3 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0274d892f2e..c8bc193a2ab 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16141,10 +16141,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
relid = con->conrelid;
else
{
- /* must be a domain constraint */
- relid = get_typ_typrelid(getBaseType(con->contypid));
- if (!OidIsValid(relid))
- elog(ERROR, "could not identify relation associated with constraint %u", oldId);
+ /*
+ * Must be a domain constraint. Domains don't have their own
+ * relations, so attach the rebuild step to the table being
+ * altered.
+ */
+ relid = tab->relid;
}
confrelid = con->confrelid;
conislocal = con->conislocal;
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..de60a90c045 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -432,6 +432,56 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
NOTICE: drop cascades to type dcomptype
+-- regression tests for bug #19724
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+ERROR: operator does not exist: text > integer
+DETAIL: No operator of that name accepts the given argument types.
+HINT: You might need to add explicit type casts.
+alter type rt alter attribute i type bigint;
+select 1::dt;
+ dt
+----
+ 1
+(1 row)
+
+select (-1)::dt; -- fail
+ERROR: value for domain dt violates check constraint "dt_check"
+drop domain dt;
+drop type rt cascade;
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+ silly
+-------
+ 1
+(1 row)
+
+select (-1.0)::silly; -- fail
+ERROR: value for domain silly violates check constraint "silly_check"
+drop domain silly;
+drop type comptype cascade;
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+ dt_multi
+----------
+ 1
+(1 row)
+
+select (-1)::dt_multi; -- fail
+ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..1240f9422bd 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -219,6 +219,39 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
+-- regression tests for bug #19724
+
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+alter type rt alter attribute i type bigint;
+select 1::dt;
+select (-1)::dt; -- fail
+drop domain dt;
+drop type rt cascade;
+
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+select (-1.0)::silly; -- fail
+drop domain silly;
+drop type comptype cascade;
+
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+select (-1)::dt_multi; -- fail
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
[text/plain] v2-0002-Validate-re-added-domain-constraints-after-ALTER-.patch (14.1K, ../../DLR67P6TO2C8.3SL4J7XR66QBJ@gmail.com/3-v2-0002-Validate-re-added-domain-constraints-after-ALTER-.patch)
download | inline diff:
From 99f8cbb9144ad3a6deb5ed110b3426efa278f522 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:05:36 -0300
Subject: [PATCH v2 2/3] Validate re-added domain constraints after ALTER TABLE
rewrites
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a domain CHECK constraint whose expression depends
on the altered column, the constraint was re-added through
AlterDomainAddConstraint(), which validates it immediately against all
columns of the domain. That happens during Phase 2, before Phase 3 has
rewritten the affected tables, so any table that is pending a rewrite
and has a column of the domain was scanned using its new tuple
descriptor over its old heap. This could produce garbage values,
spurious "contains values that violate the new constraint" errors, or
worse, e.g. "type with OID 4294967295 does not exist" when the domain is
over a composite type.
Fix by skipping validation in AlterDomainAddConstraint() when re-adding
a constraint, and instead having ATExecCmd() remember the rebuilt
constraint so that ATRewriteTables() validates it once all tables have
been rewritten. Constraints that were NOT VALID are not validated, as
before.
This problem dates back to af20e2d72, which added rebuilding of domain
constraints, but was previously only reachable with domains over
composite types, since other domains hit the "could not identify
relation associated with constraint" error instead.
---
src/backend/commands/tablecmds.c | 56 ++++++++++++++--
src/backend/commands/typecmds.c | 11 ++--
src/include/commands/typecmds.h | 1 +
src/test/regress/expected/domain.out | 99 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 63 ++++++++++++++++++
5 files changed, 221 insertions(+), 9 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c8bc193a2ab..10c7a3a5f8e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -198,6 +198,8 @@ typedef struct AlteredTableInfo
bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
char newrelpersistence; /* if above is true */
Expr *partition_constraint; /* for attach partition validation */
+ /* OIDs of re-added domain CHECK constraints to validate in Phase 3 */
+ List *domain_constraints;
/* true, if validating default due to some other attach/detach */
bool validate_default;
/* Objects to rebuild after completing ALTER TYPE operations */
@@ -5535,11 +5537,25 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
case AT_ReAddDomainConstraint: /* Re-add pre-existing domain check
* constraint */
- address =
- AlterDomainAddConstraint(((AlterDomainStmt *) cmd->def)->typeName,
- ((AlterDomainStmt *) cmd->def)->def,
- NULL, true);
- break;
+ {
+ AlterDomainStmt *stmt = (AlterDomainStmt *) cmd->def;
+ Constraint *con = castNode(Constraint, stmt->def);
+ ObjectAddress constrAddr;
+
+ address = AlterDomainAddConstraint(stmt->typeName, stmt->def,
+ &constrAddr, true);
+
+ /*
+ * AlterDomainAddConstraint doesn't validate re-added
+ * constraints, since tables using the domain may not have
+ * been rewritten yet. Tell Phase 3 to do it.
+ */
+ if (!con->skip_validation)
+ tab->domain_constraints =
+ lappend_oid(tab->domain_constraints,
+ constrAddr.objectId);
+ break;
+ }
case AT_ReAddComment: /* Re-add existing comment */
address = CommentObject((CommentStmt *) cmd->def);
break;
@@ -6160,6 +6176,36 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
table_close(rel, NoLock);
}
+ /*
+ * Validate re-added domain CHECK constraints. This must wait until all
+ * tables have been rewritten, since any of them might contain columns of
+ * the domain. Don't skip relations without storage since the work queue
+ * entry might be for a standalone composite type.
+ */
+ foreach(ltab, *wqueue)
+ {
+ AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
+
+ foreach_oid(conoid, tab->domain_constraints)
+ {
+ HeapTuple tup;
+ Form_pg_constraint con;
+ Datum conbin;
+
+ tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(conoid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for constraint %u", conoid);
+ con = (Form_pg_constraint) GETSTRUCT(tup);
+
+ conbin = SysCacheGetAttrNotNull(CONSTROID, tup,
+ Anum_pg_constraint_conbin);
+ validateDomainCheckConstraint(con->contypid,
+ TextDatumGetCString(conbin));
+
+ ReleaseSysCache(tup);
+ }
+ }
+
/* Finally, run any afterStmts that were queued up */
foreach(ltab, *wqueue)
{
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 99a0ae2228e..d0349079a1e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -128,7 +128,6 @@ static Oid findTypeSubscriptingFunction(List *procname, Oid typeOid);
static Oid findRangeSubOpclass(List *opcname, Oid subtype);
static Oid findRangeCanonicalFunction(List *procname, Oid typeOid);
static Oid findRangeSubtypeDiffFunction(List *procname, Oid subtype);
-static void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
static void validateDomainNotNullConstraint(Oid domainoid);
static List *get_rels_with_domain(Oid domainOid, LOCKMODE lockmode);
static void checkEnumOwner(HeapTuple tup);
@@ -3029,13 +3028,17 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
constr, NameStr(typTup->typname), constrAddr,
is_readd);
-
/*
* If requested to validate the constraint, test all values stored in
* the attributes based on the domain the constraint is being added
* to.
+ *
+ * When re-adding a constraint during ALTER TABLE, the tables using
+ * the domain might not have been rewritten to match their new
+ * catalog definitions yet, so the caller must do the validation after
+ * its rewrite phase instead.
*/
- if (!constr->skip_validation)
+ if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
/*
@@ -3249,7 +3252,7 @@ validateDomainNotNullConstraint(Oid domainoid)
* Verify that all columns currently using the domain satisfy the given check
* constraint expression.
*/
-static void
+void
validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
{
Expr *expr = (Expr *) stringToNode(ccbin);
diff --git a/src/include/commands/typecmds.h b/src/include/commands/typecmds.h
index 2112b4addd2..a067651f6f9 100644
--- a/src/include/commands/typecmds.h
+++ b/src/include/commands/typecmds.h
@@ -38,6 +38,7 @@ extern ObjectAddress AlterDomainAddConstraint(List *names, Node *newConstraint,
ObjectAddress *constrAddr,
bool is_readd);
extern ObjectAddress AlterDomainValidateConstraint(List *names, const char *constrName);
+extern void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
extern ObjectAddress AlterDomainDropConstraint(List *names, const char *constrName,
DropBehavior behavior, bool missing_ok);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index de60a90c045..14f2c928700 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -482,6 +482,105 @@ ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
drop domain dt_multi;
drop type r1 cascade;
drop type r2 cascade;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+ convalidated
+--------------
+ t
+(1 row)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+-----
+ 1 | (5)
+ 2 | (7)
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_ch
+drop table domrw_p cascade;
+NOTICE: drop cascades to table domrw_ch
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+ERROR: smallint out of range
+drop table domrw_u;
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+ pg_get_constraintdef | convalidated
+----------------------------------------------------------------------+--------------
+ CHECK (((ROW((VALUE)::smallint)::domrw_rt).i IS NOT NULL)) NOT VALID | f
+(1 row)
+
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 1240f9422bd..b6e452d6ebb 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -252,6 +252,69 @@ drop domain dt_multi;
drop type r1 cascade;
drop type r2 cascade;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+drop domain domrw_dt cascade;
+drop table domrw_p cascade;
+
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+drop table domrw_u;
+
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
[text/plain] v2-0003-Don-t-require-ownership-when-rebuilding-constrain.patch (8.9K, ../../DLR67P6TO2C8.3SL4J7XR66QBJ@gmail.com/4-v2-0003-Don-t-require-ownership-when-rebuilding-constrain.patch)
download | inline diff:
From bf78ab6a32c5e27572588cfa5867ed4e6f2049b6 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:18:56 -0300
Subject: [PATCH v2 3/3] Don't require ownership when rebuilding constraints in
ALTER TABLE
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a constraint that depends on the altered column,
it drops the constraint and re-creates it from its saved definition.
For domain CHECK constraints the re-creation goes through
AlterDomainAddConstraint(), which calls checkDomainOwner(), and any
comment on a table or domain constraint is restored with
CommentObject(), which requires ownership of the constraint's table or
domain. So a user altering a type or table they own would fail with
"must be owner of type ..." or "must be owner of relation ..." if
another user's domain or table has a constraint that depends on it.
Since types grant USAGE to PUBLIC by default, any user could create
such a dependency and block the owner from altering their own type.
These checks don't make sense here: the user isn't choosing to add a
constraint or comment, just restoring ones that already existed, and
the matching drop is already done without any permission checks.
Rebuilding a table constraint without a comment also doesn't check
ownership.
Fix by skipping the ownership check in AlterDomainAddConstraint() when
is_readd is set, as we already do for the USAGE check on types used
by the expression, and by restoring comments directly with
CreateComments() instead of CommentObject().
The domain part of this dates back to af20e2d72, which added
rebuilding of domain constraints.
---
src/backend/commands/tablecmds.c | 22 +++++++++++++++-
src/backend/commands/typecmds.c | 18 +++++++++----
src/test/regress/expected/domain.out | 38 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 31 +++++++++++++++++++++++
4 files changed, 103 insertions(+), 6 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 10c7a3a5f8e..a6c1d43cc0e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -5557,7 +5557,27 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
}
case AT_ReAddComment: /* Re-add existing comment */
- address = CommentObject((CommentStmt *) cmd->def);
+ {
+ CommentStmt *stmt = (CommentStmt *) cmd->def;
+ Relation comrel;
+
+ /*
+ * Don't use CommentObject(), since that requires ownership of
+ * the constraint's table or domain, which the user altering a
+ * column the constraint depends on need not have. We're just
+ * restoring a comment that already existed.
+ */
+ Assert(stmt->objtype == OBJECT_TABCONSTRAINT ||
+ stmt->objtype == OBJECT_DOMCONSTRAINT);
+ address = get_object_address(stmt->objtype, stmt->object,
+ &comrel,
+ ShareUpdateExclusiveLock,
+ false);
+ CreateComments(address.objectId, address.classId,
+ address.objectSubId, stmt->comment);
+ if (comrel != NULL)
+ relation_close(comrel, NoLock);
+ }
break;
case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
address = ATExecAddIndexConstraint(tab, rel, (IndexStmt *) cmd->def,
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index d0349079a1e..69cdfd87a79 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3004,8 +3004,16 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
elog(ERROR, "cache lookup failed for type %u", domainoid);
typTup = (Form_pg_type) GETSTRUCT(tup);
- /* Check it's a domain and check user has permission for ALTER DOMAIN */
- checkDomainOwner(tup);
+ /*
+ * Check it's a domain and check user has permission for ALTER DOMAIN.
+ * When re-adding a constraint during ALTER TABLE, skip the permission
+ * check since the constraint already existed, and the user altering a
+ * column it depends on need not own the domain.
+ */
+ if (is_readd)
+ Assert(typTup->typtype == TYPTYPE_DOMAIN);
+ else
+ checkDomainOwner(tup);
if (!IsA(newConstraint, Constraint))
elog(ERROR, "unrecognized node type: %d",
@@ -3034,9 +3042,9 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
* to.
*
* When re-adding a constraint during ALTER TABLE, the tables using
- * the domain might not have been rewritten to match their new
- * catalog definitions yet, so the caller must do the validation after
- * its rewrite phase instead.
+ * the domain might not have been rewritten to match their new catalog
+ * definitions yet, so the caller must do the validation after its
+ * rewrite phase instead.
*/
if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 14f2c928700..e49c5dacab3 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -581,6 +581,44 @@ select pg_get_constraintdef(oid), convalidated from pg_constraint
drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+ conname | pg_get_constraintdef | obj_description
+-----------------+--------------------------------------------------+-----------------------
+ domrw_dt1_check | CHECK (((ROW((VALUE)::bigint)::domrw_rt).i > 0)) | domain over int
+ domrw_dt2_check | CHECK (((VALUE).i > 0)) | domain over composite
+ domrw_t_check | CHECK (((ROW((x)::bigint)::domrw_rt).i > 0)) | table constraint
+(3 rows)
+
+select (-1)::domrw_dt1; -- fail
+ERROR: value for domain domrw_dt1 violates check constraint "domrw_dt1_check"
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b6e452d6ebb..e914b6913ee 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -315,6 +315,37 @@ drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+select (-1)::domrw_dt1; -- fail
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-28 20:40 Ayush Tiwari <ayushtiwari.slg01@gmail.com>
parent: Matheus Alcantara <matheusssilv97@gmail.com>
1 sibling, 1 reply; 11+ messages in thread
From: Ayush Tiwari @ 2026-09-28 20:40 UTC (permalink / raw)
To: Matheus Alcantara <matheusssilv97@gmail.com>; +Cc: Nitin Motiani <nitinmotiani@google.com>; pgsql-hackers
Hi,
On Tue, 29 Sept 2026 at 00:15, Matheus Alcantara
<matheusssilv97@gmail.com> wrote:
>
>
> Yes, I found the same problem while testing v1, plus a second one.
> Both date back to af20e2d72 and can already be hit on master with a
> domain over a composite type, but with v1 they are reachable for any
> domain whose check expression references a composite type.
>
> 1. Re-added domain constraints are validated before the rewrite.
>
> As you describe, AT_ReAddDomainConstraint calls
> AlterDomainAddConstraint(), which validates the constraint immediately,
> while tables using the domain may still be pending a rewrite. A simple
> way to see it:
>
> create function show(int) returns bool language plpgsql as
> $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
> create table t (c int);
> create domain dt as int check (show(value) and (null::t).c is null);
> alter table t add column d dt;
> insert into t values (1, 5), (2, 7);
> alter table t alter column c type bigint;
> NOTICE: domain check sees value 0
> NOTICE: domain check sees value 700
>
> Depending on the data this gives garbage values, spurious "contains
> values that violate the new constraint" errors, errors like "type with
> OID 4294967295 does not exist" (domain over composite, on master), or
> a crash like in your example.
> >
> > On master it just fails with the elog. (FWIW master, and 14 too as far
> > as I checked, can already crash like this with a domain over the
> > parent's rowtype stored in the child, so it's not entirely new.)
> >
> > Maybe the constraint could be re-added as NOT VALID in phase 2, and then
> > validated once phase 3 is done, e.g. with AlterDomainValidateConstraint()
> > next to the FK checks at the end of ATRewriteTables()?
> >
>
> I considered that, but I think it's simpler to skip validation when
> re-adding, remember the new constraint's OID in the work queue entry,
> and call validateDomainCheckConstraint() on it once all tables have
> been rewritten. My reasons:
>
> - AlterDomainValidateConstraint() also calls checkDomainOwner(), so it
> has issue 2 as well. Unlike AlterDomainAddConstraint() it has no
> is_readd flag, so we'd need to add a new parameter to it too.
>
> - It looks up the domain and the constraint by name. Using the OID
> avoids resolving the names again in phase 3; ATPostAlterTypeCleanup()
> already works from OIDs for similar reasons.
>
> - We'd still need to remember which constraints were valid before the
> rebuild, so that a NOT VALID constraint isn't validated. That's the
> same bookkeeping as queuing the OID.
>
> - The FK validation loop skips relations without storage. For ALTER TYPE
> on a standalone composite type the only work queue entry is the type
> itself, so validating there would silently skip it. For example, with
> a stored value of 40000, changing an attribute from int to smallint
> must still fail with "smallint out of range". The new loop on the
> attached 0002 patch runs over the whole work queue for this reason.
>
> One more thing I noticed, not addressed by these patches: if a domain
> check uses ROW(value)::t and a column of the domain is later added to
> t, the deparsed constraint becomes ROW(VALUE, NULL)::t. Re-parsing
> that coerces the NULL to the domain itself, so the rebuilt constraint
> refers to its own domain and fails with "stack depth limit exceeded".
> The same happens with a hand-written ALTER DOMAIN ... ADD CONSTRAINT
> using ROW(value, null)::t, so it's not specific to this code path,
> which is why the tests use (null::t).c instead.
>
> Attached are:
>
> - v2-0001: Nitin's v1, unchanged.
>
> - v2-0002: validate re-added domain constraints after the rewrites
> (issue 1).
Thanks, the split looks right to me.
On 0002, remembering the new constraint OID and calling
validateDomainCheckConstraint() directly is better than what I suggested.
Using the OID avoids resolving the domain and constraint by name again in
phase 3, and the standalone-composite case needs the new loop not to skip
relations without storage. One small thing: the new loop doesn't
CommandCounterIncrement() between constraints. Probably fine today, but
the FK loop and afterStmts do. And I think 0001 and 0002 can be clubbed
together (though that can be done whilst committing)
I've only looked closely at 0001 and 0002 so far, which fix the reported
case for me across branches. I'll come back on 0003.
Regards,
Ayush
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-28 21:06 Matheus Alcantara <matheusssilv97@gmail.com>
parent: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
0 siblings, 0 replies; 11+ messages in thread
From: Matheus Alcantara @ 2026-09-28 21:06 UTC (permalink / raw)
To: Ayush Tiwari <ayushtiwari.slg01@gmail.com>; +Cc: Nitin Motiani <nitinmotiani@google.com>; pgsql-hackers
On 28/09/26 17:40, Ayush Tiwari wrote:
>> Attached are:
>>
>> - v2-0001: Nitin's v1, unchanged.
>>
>> - v2-0002: validate re-added domain constraints after the rewrites
>> (issue 1).
>
> Thanks, the split looks right to me.
>
> On 0002, remembering the new constraint OID and calling
> validateDomainCheckConstraint() directly is better than what I suggested.
> Using the OID avoids resolving the domain and constraint by name again in
> phase 3, and the standalone-composite case needs the new loop not to skip
> relations without storage. One small thing: the new loop doesn't
> CommandCounterIncrement() between constraints. Probably fine today, but
> the FK loop and afterStmts do. And I think 0001 and 0002 can be clubbed
> together (though that can be done whilst committing)
>
Thank you for checking the patches.
I don't think that the FK loop call CommandCounterIncrement() or I'm
missing something? Also I think that afterStmts call it because it use
ProcessUtilityForAlterTable, so I don't think that it is required for
the new domain constraints loop, but I might be wrong.
I'm not sure if these two patches should be squashed into a single
one. I see these both issues as separated issues, although the fix on
0001 enable the second issue to happen more easily.
> I've only looked closely at 0001 and 0002 so far, which fix the reported
> case for me across branches. I'll come back on 0003.
>
Thank you!
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-28 23:02 Zsolt Parragi <zsolt.parragi@percona.com>
parent: Matheus Alcantara <matheusssilv97@gmail.com>
1 sibling, 2 replies; 11+ messages in thread
From: Zsolt Parragi @ 2026-09-28 23:02 UTC (permalink / raw)
To: Matheus Alcantara <matheusssilv97@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org, Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Hello
+ /*
+ * Check it's a domain and check user has permission for ALTER DOMAIN.
+ * When re-adding a constraint during ALTER TABLE, skip the permission
+ * check since the constraint already existed, and the user altering a
+ * column it depends on need not own the domain.
+ */
+ if (is_readd)
+ Assert(typTup->typtype == TYPTYPE_DOMAIN);
+ else
+ checkDomainOwner(tup);
That assertion can fire with two concurrent sessions, it should be a
proper error message, similar to what's inside checkDomainOwner.
See the following isolation test:
setup
{
CREATE TYPE ct AS (i int);
CREATE DOMAIN d AS ct CONSTRAINT d_check CHECK ((VALUE).i > 0);
CREATE TABLE t2 (x int CONSTRAINT t2_check CHECK ((row(x)::ct).i > 0));
INSERT INTO t2 VALUES (1);
}
teardown
{
DROP TABLE IF EXISTS t2;
DROP TYPE IF EXISTS d CASCADE;
DROP DOMAIN IF EXISTS d_old CASCADE;
DROP TYPE IF EXISTS ct CASCADE;
}
session s1
step a_alter { ALTER TYPE ct ALTER ATTRIBUTE i TYPE bigint; }
session s2
step b_begin { BEGIN; SELECT count(*) FROM t2; }
step b_commit { COMMIT; }
session s3
step c_swap { ALTER DOMAIN d RENAME TO d_old; CREATE TYPE d AS (z int); }
permutation b_begin a_alter c_swap b_commit
+ if (!con->skip_validation)
+ tab->domain_constraints =
+ lappend_oid(tab->domain_constraints,
+ constrAddr.objectId);
This can be uninitialized, AlterDomainAddConstraint doesn't guarantee
a write. I think this could use both a Assert(con->contype ==
CONSTR_CHECK); and initalizating constrAddr to InvalidObjectAddress.
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-29 07:03 Nitin Motiani <nitinmotiani@google.com>
parent: Zsolt Parragi <zsolt.parragi@percona.com>
1 sibling, 0 replies; 11+ messages in thread
From: Nitin Motiani @ 2026-09-29 07:03 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: Matheus Alcantara <matheusssilv97@gmail.com>; pgsql-hackers@lists.postgresql.org, Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Thanks everyone for the feedback. I'll work on incorporating that.
I'm also looking into 0002 and 0003 from Matheus. From a quick scan,
it looks like those issues have existed since earlier, even in the
cases involving domains over composite types. So perhaps it's better
to have them as separate patches. But I'll look in more detail to see
if 0001 and 0002 should be squashed together.
Regards,
Nitin Motiani
Google
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-09-29 13:54 Matheus Alcantara <matheusssilv97@gmail.com>
parent: Zsolt Parragi <zsolt.parragi@percona.com>
1 sibling, 1 reply; 11+ messages in thread
From: Matheus Alcantara @ 2026-09-29 13:54 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: pgsql-hackers@lists.postgresql.org; Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Thank you for reviewing the patch!
On 28/09/26 20:02, Zsolt Parragi wrote:
> Hello
>
> + /*
> + * Check it's a domain and check user has permission for ALTER DOMAIN.
> + * When re-adding a constraint during ALTER TABLE, skip the permission
> + * check since the constraint already existed, and the user altering a
> + * column it depends on need not own the domain.
> + */
> + if (is_readd)
> + Assert(typTup->typtype == TYPTYPE_DOMAIN);
> + else
> + checkDomainOwner(tup);
>
> That assertion can fire with two concurrent sessions, it should be a
> proper error message, similar to what's inside checkDomainOwner.
>
> See the following isolation test:
>
> setup
> {
> CREATE TYPE ct AS (i int);
> CREATE DOMAIN d AS ct CONSTRAINT d_check CHECK ((VALUE).i > 0);
> CREATE TABLE t2 (x int CONSTRAINT t2_check CHECK ((row(x)::ct).i > 0));
> INSERT INTO t2 VALUES (1);
> }
>
> teardown
> {
> DROP TABLE IF EXISTS t2;
> DROP TYPE IF EXISTS d CASCADE;
> DROP DOMAIN IF EXISTS d_old CASCADE;
> DROP TYPE IF EXISTS ct CASCADE;
> }
>
> session s1
> step a_alter { ALTER TYPE ct ALTER ATTRIBUTE i TYPE bigint; }
>
> session s2
> step b_begin { BEGIN; SELECT count(*) FROM t2; }
> step b_commit { COMMIT; }
>
> session s3
> step c_swap { ALTER DOMAIN d RENAME TO d_old; CREATE TYPE d AS (z int); }
>
> permutation b_begin a_alter c_swap b_commit
>
>
Good catch. I've changed to use ereport like checkDomainOwner().
But I don't think that's enough. The underlying issue is that the re-add
looks the domain up by the name saved in its definition, so the name can
point to a different type by the time the constraint is re-added. In
your isolation test example, if c_swap creates a new domain instead
(CREATE DOMAIN d AS ct), the type check passes, the ALTER succeeds, and
d_check silently ends up on the new domain, while the original (now
d_old) loses it.
Note that this isn't new with the patch. What 0003 changes is that
without the ownership check, it also works when the domain belongs to
someone other than the user running the ALTER.
We may try to capture the domain oid above AlterDomainAddConstraint,
while the old constraint still exists and re-add the constraint to that
OID instead of resolving the name again. But I think that it will
require more code to write which would make it harder for back patching.
Looking for thoughts here.
> + if (!con->skip_validation)
> + tab->domain_constraints =
> + lappend_oid(tab->domain_constraints,
> + constrAddr.objectId);
>
> This can be uninitialized, AlterDomainAddConstraint doesn't guarantee
> a write. I think this could use both a Assert(con->contype ==
> CONSTR_CHECK); and initalizating constrAddr to InvalidObjectAddress.
Fixed.
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
From a1296adad6923059274fad35de53200b5b1372c2 Mon Sep 17 00:00:00 2001
From: Nitin Motiani <nitinmotiani@google.com>
Date: Mon, 28 Sep 2026 12:49:33 +0000
Subject: [PATCH v3 1/3] Fix ALTER TYPE ... ALTER ATTRIBUTE on types used in
domain constraints.
Commit af20e2d72 updated ALTER TABLE / TYPE to rebuild domain constraints
when an attribute of a composite type is altered. However, it assumed that
the domain's base type was always the composite type being altered, calling
get_typ_typrelid(getBaseType(con->contypid)). If the domain was defined
over a scalar type (such as int or float8) whose CHECK expression referenced
the composite type, get_typ_typrelid() returned InvalidOid, triggering
an internal "could not identify relation associated with constraint" error.
Fix by attaching the deferred domain constraint rebuild command to the
table being altered (tab->relid) rather than attempting to derive a relation
OID from the domain's base type. Domains do not have pg_class relations of
their own, and the rebuild command (AlterDomainStmt) is self-contained.
Reported-by: Alexander Lakhin
Bug: #19724
---
src/backend/commands/tablecmds.c | 10 +++---
src/test/regress/expected/domain.out | 50 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 33 ++++++++++++++++++
3 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0274d892f2e..c8bc193a2ab 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16141,10 +16141,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
relid = con->conrelid;
else
{
- /* must be a domain constraint */
- relid = get_typ_typrelid(getBaseType(con->contypid));
- if (!OidIsValid(relid))
- elog(ERROR, "could not identify relation associated with constraint %u", oldId);
+ /*
+ * Must be a domain constraint. Domains don't have their own
+ * relations, so attach the rebuild step to the table being
+ * altered.
+ */
+ relid = tab->relid;
}
confrelid = con->confrelid;
conislocal = con->conislocal;
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..de60a90c045 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -432,6 +432,56 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
NOTICE: drop cascades to type dcomptype
+-- regression tests for bug #19724
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+ERROR: operator does not exist: text > integer
+DETAIL: No operator of that name accepts the given argument types.
+HINT: You might need to add explicit type casts.
+alter type rt alter attribute i type bigint;
+select 1::dt;
+ dt
+----
+ 1
+(1 row)
+
+select (-1)::dt; -- fail
+ERROR: value for domain dt violates check constraint "dt_check"
+drop domain dt;
+drop type rt cascade;
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+ silly
+-------
+ 1
+(1 row)
+
+select (-1.0)::silly; -- fail
+ERROR: value for domain silly violates check constraint "silly_check"
+drop domain silly;
+drop type comptype cascade;
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+ dt_multi
+----------
+ 1
+(1 row)
+
+select (-1)::dt_multi; -- fail
+ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..1240f9422bd 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -219,6 +219,39 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
+-- regression tests for bug #19724
+
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+alter type rt alter attribute i type bigint;
+select 1::dt;
+select (-1)::dt; -- fail
+drop domain dt;
+drop type rt cascade;
+
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+select (-1.0)::silly; -- fail
+drop domain silly;
+drop type comptype cascade;
+
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+select (-1)::dt_multi; -- fail
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
From 1cf5d63cd2ebc1f0e4826794ec308382b1e9952c Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:05:36 -0300
Subject: [PATCH v3 2/3] Validate re-added domain constraints after ALTER TABLE
rewrites
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a domain CHECK constraint whose expression depends
on the altered column, the constraint was re-added through
AlterDomainAddConstraint(), which validates it immediately against all
columns of the domain. That happens during Phase 2, before Phase 3 has
rewritten the affected tables, so any table that is pending a rewrite
and has a column of the domain was scanned using its new tuple
descriptor over its old heap. This could produce garbage values,
spurious "contains values that violate the new constraint" errors, or
worse, e.g. "type with OID 4294967295 does not exist" when the domain is
over a composite type.
Fix by skipping validation in AlterDomainAddConstraint() when re-adding
a constraint, and instead having ATExecCmd() remember the rebuilt
constraint so that ATRewriteTables() validates it once all tables have
been rewritten. Constraints that were NOT VALID are not validated, as
before.
This problem dates back to af20e2d72, which added rebuilding of domain
constraints, but was previously only reachable with domains over
composite types, since other domains hit the "could not identify
relation associated with constraint" error instead.
---
src/backend/commands/tablecmds.c | 59 +++++++++++++++--
src/backend/commands/typecmds.c | 11 ++--
src/include/commands/typecmds.h | 1 +
src/test/regress/expected/domain.out | 99 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 63 ++++++++++++++++++
5 files changed, 224 insertions(+), 9 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c8bc193a2ab..d93c2e0d452 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -198,6 +198,8 @@ typedef struct AlteredTableInfo
bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
char newrelpersistence; /* if above is true */
Expr *partition_constraint; /* for attach partition validation */
+ /* OIDs of re-added domain CHECK constraints to validate in Phase 3 */
+ List *domain_constraints;
/* true, if validating default due to some other attach/detach */
bool validate_default;
/* Objects to rebuild after completing ALTER TYPE operations */
@@ -5535,11 +5537,28 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
case AT_ReAddDomainConstraint: /* Re-add pre-existing domain check
* constraint */
- address =
- AlterDomainAddConstraint(((AlterDomainStmt *) cmd->def)->typeName,
- ((AlterDomainStmt *) cmd->def)->def,
- NULL, true);
- break;
+ {
+ AlterDomainStmt *stmt = (AlterDomainStmt *) cmd->def;
+ Constraint *con = castNode(Constraint, stmt->def);
+ ObjectAddress constrAddr = InvalidObjectAddress;
+
+ /* only CHECK constraints can depend on a column */
+ Assert(con->contype == CONSTR_CHECK);
+
+ address = AlterDomainAddConstraint(stmt->typeName, stmt->def,
+ &constrAddr, true);
+
+ /*
+ * AlterDomainAddConstraint doesn't validate re-added
+ * constraints, since tables using the domain may not have
+ * been rewritten yet. Tell Phase 3 to do it.
+ */
+ if (!con->skip_validation)
+ tab->domain_constraints =
+ lappend_oid(tab->domain_constraints,
+ constrAddr.objectId);
+ break;
+ }
case AT_ReAddComment: /* Re-add existing comment */
address = CommentObject((CommentStmt *) cmd->def);
break;
@@ -6160,6 +6179,36 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
table_close(rel, NoLock);
}
+ /*
+ * Validate re-added domain CHECK constraints. This must wait until all
+ * tables have been rewritten, since any of them might contain columns of
+ * the domain. Don't skip relations without storage since the work queue
+ * entry might be for a standalone composite type.
+ */
+ foreach(ltab, *wqueue)
+ {
+ AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
+
+ foreach_oid(conoid, tab->domain_constraints)
+ {
+ HeapTuple tup;
+ Form_pg_constraint con;
+ Datum conbin;
+
+ tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(conoid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for constraint %u", conoid);
+ con = (Form_pg_constraint) GETSTRUCT(tup);
+
+ conbin = SysCacheGetAttrNotNull(CONSTROID, tup,
+ Anum_pg_constraint_conbin);
+ validateDomainCheckConstraint(con->contypid,
+ TextDatumGetCString(conbin));
+
+ ReleaseSysCache(tup);
+ }
+ }
+
/* Finally, run any afterStmts that were queued up */
foreach(ltab, *wqueue)
{
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 99a0ae2228e..d0349079a1e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -128,7 +128,6 @@ static Oid findTypeSubscriptingFunction(List *procname, Oid typeOid);
static Oid findRangeSubOpclass(List *opcname, Oid subtype);
static Oid findRangeCanonicalFunction(List *procname, Oid typeOid);
static Oid findRangeSubtypeDiffFunction(List *procname, Oid subtype);
-static void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
static void validateDomainNotNullConstraint(Oid domainoid);
static List *get_rels_with_domain(Oid domainOid, LOCKMODE lockmode);
static void checkEnumOwner(HeapTuple tup);
@@ -3029,13 +3028,17 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
constr, NameStr(typTup->typname), constrAddr,
is_readd);
-
/*
* If requested to validate the constraint, test all values stored in
* the attributes based on the domain the constraint is being added
* to.
+ *
+ * When re-adding a constraint during ALTER TABLE, the tables using
+ * the domain might not have been rewritten to match their new
+ * catalog definitions yet, so the caller must do the validation after
+ * its rewrite phase instead.
*/
- if (!constr->skip_validation)
+ if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
/*
@@ -3249,7 +3252,7 @@ validateDomainNotNullConstraint(Oid domainoid)
* Verify that all columns currently using the domain satisfy the given check
* constraint expression.
*/
-static void
+void
validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
{
Expr *expr = (Expr *) stringToNode(ccbin);
diff --git a/src/include/commands/typecmds.h b/src/include/commands/typecmds.h
index 2112b4addd2..a067651f6f9 100644
--- a/src/include/commands/typecmds.h
+++ b/src/include/commands/typecmds.h
@@ -38,6 +38,7 @@ extern ObjectAddress AlterDomainAddConstraint(List *names, Node *newConstraint,
ObjectAddress *constrAddr,
bool is_readd);
extern ObjectAddress AlterDomainValidateConstraint(List *names, const char *constrName);
+extern void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
extern ObjectAddress AlterDomainDropConstraint(List *names, const char *constrName,
DropBehavior behavior, bool missing_ok);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index de60a90c045..14f2c928700 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -482,6 +482,105 @@ ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
drop domain dt_multi;
drop type r1 cascade;
drop type r2 cascade;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+ convalidated
+--------------
+ t
+(1 row)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+-----
+ 1 | (5)
+ 2 | (7)
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_ch
+drop table domrw_p cascade;
+NOTICE: drop cascades to table domrw_ch
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+ERROR: smallint out of range
+drop table domrw_u;
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+ pg_get_constraintdef | convalidated
+----------------------------------------------------------------------+--------------
+ CHECK (((ROW((VALUE)::smallint)::domrw_rt).i IS NOT NULL)) NOT VALID | f
+(1 row)
+
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 1240f9422bd..b6e452d6ebb 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -252,6 +252,69 @@ drop domain dt_multi;
drop type r1 cascade;
drop type r2 cascade;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+drop domain domrw_dt cascade;
+drop table domrw_p cascade;
+
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+drop table domrw_u;
+
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
From 06a359a989b3124a3acca50d22f878ffee72e777 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:18:56 -0300
Subject: [PATCH v3 3/3] Don't require ownership when rebuilding constraints in
ALTER TABLE
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a constraint that depends on the altered column,
it drops the constraint and re-creates it from its saved definition.
For domain CHECK constraints the re-creation goes through
AlterDomainAddConstraint(), which calls checkDomainOwner(), and any
comment on a table or domain constraint is restored with
CommentObject(), which requires ownership of the constraint's table or
domain. So a user altering a type or table they own would fail with
"must be owner of type ..." or "must be owner of relation ..." if
another user's domain or table has a constraint that depends on it.
Since types grant USAGE to PUBLIC by default, any user could create
such a dependency and block the owner from altering their own type.
These checks don't make sense here: the user isn't choosing to add a
constraint or comment, just restoring ones that already existed, and
the matching drop is already done without any permission checks.
Rebuilding a table constraint without a comment also doesn't check
ownership.
Fix by skipping the ownership check in AlterDomainAddConstraint() when
is_readd is set, as we already do for the USAGE check on types used
by the expression, and by restoring comments directly with
CreateComments() instead of CommentObject().
The domain part of this dates back to af20e2d72, which added
rebuilding of domain constraints.
---
src/backend/commands/tablecmds.c | 22 +++++++++++++++-
src/backend/commands/typecmds.c | 24 ++++++++++++++----
src/test/regress/expected/domain.out | 38 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 31 +++++++++++++++++++++++
4 files changed, 109 insertions(+), 6 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d93c2e0d452..1a5bb53e1f3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -5560,7 +5560,27 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
}
case AT_ReAddComment: /* Re-add existing comment */
- address = CommentObject((CommentStmt *) cmd->def);
+ {
+ CommentStmt *stmt = (CommentStmt *) cmd->def;
+ Relation comrel;
+
+ /*
+ * Don't use CommentObject(), since that requires ownership of
+ * the constraint's table or domain, which the user altering a
+ * column the constraint depends on need not have. We're just
+ * restoring a comment that already existed.
+ */
+ Assert(stmt->objtype == OBJECT_TABCONSTRAINT ||
+ stmt->objtype == OBJECT_DOMCONSTRAINT);
+ address = get_object_address(stmt->objtype, stmt->object,
+ &comrel,
+ ShareUpdateExclusiveLock,
+ false);
+ CreateComments(address.objectId, address.classId,
+ address.objectSubId, stmt->comment);
+ if (comrel != NULL)
+ relation_close(comrel, NoLock);
+ }
break;
case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
address = ATExecAddIndexConstraint(tab, rel, (IndexStmt *) cmd->def,
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index d0349079a1e..7e9846bda0e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3004,8 +3004,22 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
elog(ERROR, "cache lookup failed for type %u", domainoid);
typTup = (Form_pg_type) GETSTRUCT(tup);
- /* Check it's a domain and check user has permission for ALTER DOMAIN */
- checkDomainOwner(tup);
+ /*
+ * Check it's a domain and check user has permission for ALTER DOMAIN.
+ * When re-adding a constraint during ALTER TABLE, skip the permission
+ * check since the constraint already existed, and the user altering a
+ * column it depends on need not own the domain.
+ */
+ if (is_readd)
+ {
+ if (typTup->typtype != TYPTYPE_DOMAIN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("%s is not a domain",
+ format_type_be(typTup->oid))));
+ }
+ else
+ checkDomainOwner(tup);
if (!IsA(newConstraint, Constraint))
elog(ERROR, "unrecognized node type: %d",
@@ -3034,9 +3048,9 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
* to.
*
* When re-adding a constraint during ALTER TABLE, the tables using
- * the domain might not have been rewritten to match their new
- * catalog definitions yet, so the caller must do the validation after
- * its rewrite phase instead.
+ * the domain might not have been rewritten to match their new catalog
+ * definitions yet, so the caller must do the validation after its
+ * rewrite phase instead.
*/
if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 14f2c928700..e49c5dacab3 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -581,6 +581,44 @@ select pg_get_constraintdef(oid), convalidated from pg_constraint
drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+ conname | pg_get_constraintdef | obj_description
+-----------------+--------------------------------------------------+-----------------------
+ domrw_dt1_check | CHECK (((ROW((VALUE)::bigint)::domrw_rt).i > 0)) | domain over int
+ domrw_dt2_check | CHECK (((VALUE).i > 0)) | domain over composite
+ domrw_t_check | CHECK (((ROW((x)::bigint)::domrw_rt).i > 0)) | table constraint
+(3 rows)
+
+select (-1)::domrw_dt1; -- fail
+ERROR: value for domain domrw_dt1 violates check constraint "domrw_dt1_check"
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b6e452d6ebb..e914b6913ee 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -315,6 +315,37 @@ drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+select (-1)::domrw_dt1; -- fail
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v3-0001-Fix-ALTER-TYPE-.-ALTER-ATTRIBUTE-on-types-used-in.patch (5.7K, ../../DLRUNGYX33TS.2M17VTR2OBXPC@gmail.com/2-v3-0001-Fix-ALTER-TYPE-.-ALTER-ATTRIBUTE-on-types-used-in.patch)
download | inline diff:
From a1296adad6923059274fad35de53200b5b1372c2 Mon Sep 17 00:00:00 2001
From: Nitin Motiani <nitinmotiani@google.com>
Date: Mon, 28 Sep 2026 12:49:33 +0000
Subject: [PATCH v3 1/3] Fix ALTER TYPE ... ALTER ATTRIBUTE on types used in
domain constraints.
Commit af20e2d72 updated ALTER TABLE / TYPE to rebuild domain constraints
when an attribute of a composite type is altered. However, it assumed that
the domain's base type was always the composite type being altered, calling
get_typ_typrelid(getBaseType(con->contypid)). If the domain was defined
over a scalar type (such as int or float8) whose CHECK expression referenced
the composite type, get_typ_typrelid() returned InvalidOid, triggering
an internal "could not identify relation associated with constraint" error.
Fix by attaching the deferred domain constraint rebuild command to the
table being altered (tab->relid) rather than attempting to derive a relation
OID from the domain's base type. Domains do not have pg_class relations of
their own, and the rebuild command (AlterDomainStmt) is self-contained.
Reported-by: Alexander Lakhin
Bug: #19724
---
src/backend/commands/tablecmds.c | 10 +++---
src/test/regress/expected/domain.out | 50 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 33 ++++++++++++++++++
3 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0274d892f2e..c8bc193a2ab 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16141,10 +16141,12 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
relid = con->conrelid;
else
{
- /* must be a domain constraint */
- relid = get_typ_typrelid(getBaseType(con->contypid));
- if (!OidIsValid(relid))
- elog(ERROR, "could not identify relation associated with constraint %u", oldId);
+ /*
+ * Must be a domain constraint. Domains don't have their own
+ * relations, so attach the rebuild step to the table being
+ * altered.
+ */
+ relid = tab->relid;
}
confrelid = con->confrelid;
conislocal = con->conislocal;
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..de60a90c045 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -432,6 +432,56 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
NOTICE: drop cascades to type dcomptype
+-- regression tests for bug #19724
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+ERROR: operator does not exist: text > integer
+DETAIL: No operator of that name accepts the given argument types.
+HINT: You might need to add explicit type casts.
+alter type rt alter attribute i type bigint;
+select 1::dt;
+ dt
+----
+ 1
+(1 row)
+
+select (-1)::dt; -- fail
+ERROR: value for domain dt violates check constraint "dt_check"
+drop domain dt;
+drop type rt cascade;
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+ silly
+-------
+ 1
+(1 row)
+
+select (-1.0)::silly; -- fail
+ERROR: value for domain silly violates check constraint "silly_check"
+drop domain silly;
+drop type comptype cascade;
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+ dt_multi
+----------
+ 1
+(1 row)
+
+select (-1)::dt_multi; -- fail
+ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..1240f9422bd 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -219,6 +219,39 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
+-- regression tests for bug #19724
+
+-- test scenario from bug report, plus failure when changing int to text
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+alter type rt alter attribute i type bigint;
+select 1::dt;
+select (-1)::dt; -- fail
+drop domain dt;
+drop type rt cascade;
+
+-- test silly example from Tom Lane's 2017 email (domain over float8)
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+select (-1.0)::silly; -- fail
+drop domain silly;
+drop type comptype cascade;
+
+-- test domain constraint referencing multiple composite types
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+select (-1)::dt_multi; -- fail
+drop domain dt_multi;
+drop type r1 cascade;
+drop type r2 cascade;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
[text/plain] v3-0002-Validate-re-added-domain-constraints-after-ALTER-.patch (14.2K, ../../DLRUNGYX33TS.2M17VTR2OBXPC@gmail.com/3-v3-0002-Validate-re-added-domain-constraints-after-ALTER-.patch)
download | inline diff:
From 1cf5d63cd2ebc1f0e4826794ec308382b1e9952c Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:05:36 -0300
Subject: [PATCH v3 2/3] Validate re-added domain constraints after ALTER TABLE
rewrites
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a domain CHECK constraint whose expression depends
on the altered column, the constraint was re-added through
AlterDomainAddConstraint(), which validates it immediately against all
columns of the domain. That happens during Phase 2, before Phase 3 has
rewritten the affected tables, so any table that is pending a rewrite
and has a column of the domain was scanned using its new tuple
descriptor over its old heap. This could produce garbage values,
spurious "contains values that violate the new constraint" errors, or
worse, e.g. "type with OID 4294967295 does not exist" when the domain is
over a composite type.
Fix by skipping validation in AlterDomainAddConstraint() when re-adding
a constraint, and instead having ATExecCmd() remember the rebuilt
constraint so that ATRewriteTables() validates it once all tables have
been rewritten. Constraints that were NOT VALID are not validated, as
before.
This problem dates back to af20e2d72, which added rebuilding of domain
constraints, but was previously only reachable with domains over
composite types, since other domains hit the "could not identify
relation associated with constraint" error instead.
---
src/backend/commands/tablecmds.c | 59 +++++++++++++++--
src/backend/commands/typecmds.c | 11 ++--
src/include/commands/typecmds.h | 1 +
src/test/regress/expected/domain.out | 99 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 63 ++++++++++++++++++
5 files changed, 224 insertions(+), 9 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c8bc193a2ab..d93c2e0d452 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -198,6 +198,8 @@ typedef struct AlteredTableInfo
bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
char newrelpersistence; /* if above is true */
Expr *partition_constraint; /* for attach partition validation */
+ /* OIDs of re-added domain CHECK constraints to validate in Phase 3 */
+ List *domain_constraints;
/* true, if validating default due to some other attach/detach */
bool validate_default;
/* Objects to rebuild after completing ALTER TYPE operations */
@@ -5535,11 +5537,28 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
case AT_ReAddDomainConstraint: /* Re-add pre-existing domain check
* constraint */
- address =
- AlterDomainAddConstraint(((AlterDomainStmt *) cmd->def)->typeName,
- ((AlterDomainStmt *) cmd->def)->def,
- NULL, true);
- break;
+ {
+ AlterDomainStmt *stmt = (AlterDomainStmt *) cmd->def;
+ Constraint *con = castNode(Constraint, stmt->def);
+ ObjectAddress constrAddr = InvalidObjectAddress;
+
+ /* only CHECK constraints can depend on a column */
+ Assert(con->contype == CONSTR_CHECK);
+
+ address = AlterDomainAddConstraint(stmt->typeName, stmt->def,
+ &constrAddr, true);
+
+ /*
+ * AlterDomainAddConstraint doesn't validate re-added
+ * constraints, since tables using the domain may not have
+ * been rewritten yet. Tell Phase 3 to do it.
+ */
+ if (!con->skip_validation)
+ tab->domain_constraints =
+ lappend_oid(tab->domain_constraints,
+ constrAddr.objectId);
+ break;
+ }
case AT_ReAddComment: /* Re-add existing comment */
address = CommentObject((CommentStmt *) cmd->def);
break;
@@ -6160,6 +6179,36 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
table_close(rel, NoLock);
}
+ /*
+ * Validate re-added domain CHECK constraints. This must wait until all
+ * tables have been rewritten, since any of them might contain columns of
+ * the domain. Don't skip relations without storage since the work queue
+ * entry might be for a standalone composite type.
+ */
+ foreach(ltab, *wqueue)
+ {
+ AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
+
+ foreach_oid(conoid, tab->domain_constraints)
+ {
+ HeapTuple tup;
+ Form_pg_constraint con;
+ Datum conbin;
+
+ tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(conoid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for constraint %u", conoid);
+ con = (Form_pg_constraint) GETSTRUCT(tup);
+
+ conbin = SysCacheGetAttrNotNull(CONSTROID, tup,
+ Anum_pg_constraint_conbin);
+ validateDomainCheckConstraint(con->contypid,
+ TextDatumGetCString(conbin));
+
+ ReleaseSysCache(tup);
+ }
+ }
+
/* Finally, run any afterStmts that were queued up */
foreach(ltab, *wqueue)
{
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 99a0ae2228e..d0349079a1e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -128,7 +128,6 @@ static Oid findTypeSubscriptingFunction(List *procname, Oid typeOid);
static Oid findRangeSubOpclass(List *opcname, Oid subtype);
static Oid findRangeCanonicalFunction(List *procname, Oid typeOid);
static Oid findRangeSubtypeDiffFunction(List *procname, Oid subtype);
-static void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
static void validateDomainNotNullConstraint(Oid domainoid);
static List *get_rels_with_domain(Oid domainOid, LOCKMODE lockmode);
static void checkEnumOwner(HeapTuple tup);
@@ -3029,13 +3028,17 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
constr, NameStr(typTup->typname), constrAddr,
is_readd);
-
/*
* If requested to validate the constraint, test all values stored in
* the attributes based on the domain the constraint is being added
* to.
+ *
+ * When re-adding a constraint during ALTER TABLE, the tables using
+ * the domain might not have been rewritten to match their new
+ * catalog definitions yet, so the caller must do the validation after
+ * its rewrite phase instead.
*/
- if (!constr->skip_validation)
+ if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
/*
@@ -3249,7 +3252,7 @@ validateDomainNotNullConstraint(Oid domainoid)
* Verify that all columns currently using the domain satisfy the given check
* constraint expression.
*/
-static void
+void
validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
{
Expr *expr = (Expr *) stringToNode(ccbin);
diff --git a/src/include/commands/typecmds.h b/src/include/commands/typecmds.h
index 2112b4addd2..a067651f6f9 100644
--- a/src/include/commands/typecmds.h
+++ b/src/include/commands/typecmds.h
@@ -38,6 +38,7 @@ extern ObjectAddress AlterDomainAddConstraint(List *names, Node *newConstraint,
ObjectAddress *constrAddr,
bool is_readd);
extern ObjectAddress AlterDomainValidateConstraint(List *names, const char *constrName);
+extern void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
extern ObjectAddress AlterDomainDropConstraint(List *names, const char *constrName,
DropBehavior behavior, bool missing_ok);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index de60a90c045..14f2c928700 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -482,6 +482,105 @@ ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
drop domain dt_multi;
drop type r1 cascade;
drop type r2 cascade;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+ convalidated
+--------------
+ t
+(1 row)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+-----
+ 1 | (5)
+ 2 | (7)
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_ch
+drop table domrw_p cascade;
+NOTICE: drop cascades to table domrw_ch
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+ERROR: smallint out of range
+drop table domrw_u;
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+ pg_get_constraintdef | convalidated
+----------------------------------------------------------------------+--------------
+ CHECK (((ROW((VALUE)::smallint)::domrw_rt).i IS NOT NULL)) NOT VALID | f
+(1 row)
+
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index 1240f9422bd..b6e452d6ebb 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -252,6 +252,69 @@ drop domain dt_multi;
drop type r1 cascade;
drop type r2 cascade;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+drop domain domrw_dt cascade;
+drop table domrw_p cascade;
+
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+drop table domrw_u;
+
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
[text/plain] v3-0003-Don-t-require-ownership-when-rebuilding-constrain.patch (9.1K, ../../DLRUNGYX33TS.2M17VTR2OBXPC@gmail.com/4-v3-0003-Don-t-require-ownership-when-rebuilding-constrain.patch)
download | inline diff:
From 06a359a989b3124a3acca50d22f878ffee72e777 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:18:56 -0300
Subject: [PATCH v3 3/3] Don't require ownership when rebuilding constraints in
ALTER TABLE
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a constraint that depends on the altered column,
it drops the constraint and re-creates it from its saved definition.
For domain CHECK constraints the re-creation goes through
AlterDomainAddConstraint(), which calls checkDomainOwner(), and any
comment on a table or domain constraint is restored with
CommentObject(), which requires ownership of the constraint's table or
domain. So a user altering a type or table they own would fail with
"must be owner of type ..." or "must be owner of relation ..." if
another user's domain or table has a constraint that depends on it.
Since types grant USAGE to PUBLIC by default, any user could create
such a dependency and block the owner from altering their own type.
These checks don't make sense here: the user isn't choosing to add a
constraint or comment, just restoring ones that already existed, and
the matching drop is already done without any permission checks.
Rebuilding a table constraint without a comment also doesn't check
ownership.
Fix by skipping the ownership check in AlterDomainAddConstraint() when
is_readd is set, as we already do for the USAGE check on types used
by the expression, and by restoring comments directly with
CreateComments() instead of CommentObject().
The domain part of this dates back to af20e2d72, which added
rebuilding of domain constraints.
---
src/backend/commands/tablecmds.c | 22 +++++++++++++++-
src/backend/commands/typecmds.c | 24 ++++++++++++++----
src/test/regress/expected/domain.out | 38 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 31 +++++++++++++++++++++++
4 files changed, 109 insertions(+), 6 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d93c2e0d452..1a5bb53e1f3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -5560,7 +5560,27 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
}
case AT_ReAddComment: /* Re-add existing comment */
- address = CommentObject((CommentStmt *) cmd->def);
+ {
+ CommentStmt *stmt = (CommentStmt *) cmd->def;
+ Relation comrel;
+
+ /*
+ * Don't use CommentObject(), since that requires ownership of
+ * the constraint's table or domain, which the user altering a
+ * column the constraint depends on need not have. We're just
+ * restoring a comment that already existed.
+ */
+ Assert(stmt->objtype == OBJECT_TABCONSTRAINT ||
+ stmt->objtype == OBJECT_DOMCONSTRAINT);
+ address = get_object_address(stmt->objtype, stmt->object,
+ &comrel,
+ ShareUpdateExclusiveLock,
+ false);
+ CreateComments(address.objectId, address.classId,
+ address.objectSubId, stmt->comment);
+ if (comrel != NULL)
+ relation_close(comrel, NoLock);
+ }
break;
case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
address = ATExecAddIndexConstraint(tab, rel, (IndexStmt *) cmd->def,
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index d0349079a1e..7e9846bda0e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3004,8 +3004,22 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
elog(ERROR, "cache lookup failed for type %u", domainoid);
typTup = (Form_pg_type) GETSTRUCT(tup);
- /* Check it's a domain and check user has permission for ALTER DOMAIN */
- checkDomainOwner(tup);
+ /*
+ * Check it's a domain and check user has permission for ALTER DOMAIN.
+ * When re-adding a constraint during ALTER TABLE, skip the permission
+ * check since the constraint already existed, and the user altering a
+ * column it depends on need not own the domain.
+ */
+ if (is_readd)
+ {
+ if (typTup->typtype != TYPTYPE_DOMAIN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("%s is not a domain",
+ format_type_be(typTup->oid))));
+ }
+ else
+ checkDomainOwner(tup);
if (!IsA(newConstraint, Constraint))
elog(ERROR, "unrecognized node type: %d",
@@ -3034,9 +3048,9 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
* to.
*
* When re-adding a constraint during ALTER TABLE, the tables using
- * the domain might not have been rewritten to match their new
- * catalog definitions yet, so the caller must do the validation after
- * its rewrite phase instead.
+ * the domain might not have been rewritten to match their new catalog
+ * definitions yet, so the caller must do the validation after its
+ * rewrite phase instead.
*/
if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 14f2c928700..e49c5dacab3 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -581,6 +581,44 @@ select pg_get_constraintdef(oid), convalidated from pg_constraint
drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+ conname | pg_get_constraintdef | obj_description
+-----------------+--------------------------------------------------+-----------------------
+ domrw_dt1_check | CHECK (((ROW((VALUE)::bigint)::domrw_rt).i > 0)) | domain over int
+ domrw_dt2_check | CHECK (((VALUE).i > 0)) | domain over composite
+ domrw_t_check | CHECK (((ROW((x)::bigint)::domrw_rt).i > 0)) | table constraint
+(3 rows)
+
+select (-1)::domrw_dt1; -- fail
+ERROR: value for domain domrw_dt1 violates check constraint "domrw_dt1_check"
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b6e452d6ebb..e914b6913ee 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -315,6 +315,37 @@ drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+select (-1)::domrw_dt1; -- fail
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
+
-- Test domains over arrays of composite
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 11+ messages in thread
* Re: [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check
@ 2026-10-03 10:20 Nitin Motiani <nitinmotiani@google.com>
parent: Matheus Alcantara <matheusssilv97@gmail.com>
0 siblings, 0 replies; 11+ messages in thread
From: Nitin Motiani @ 2026-10-03 10:20 UTC (permalink / raw)
To: Matheus Alcantara <matheusssilv97@gmail.com>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Ayush Tiwari <ayushtiwari.slg01@gmail.com>
I'm adding the v4 patches. Matheus's 0002 and 0003 are rebased on top of 0001.
I've amended the commit message to include ALTER TABLE, ALTER COLUMN
etc. Have edited some of the comments in the test. Have also added a
comment to the actual code explaining why tab->relid is safe.
I haven't removed tests but made some changes like removing cascade
from one of them. Have changed the test with multiple relations to now
have one invalid alter. Mainly I wanted to test that after the first
ALTER succeeds and recreates the constraint, the constraint is
correctly recreated for the other relation and still fails if an
incompatible type is used. I kept one of the tests with float8 because
it was mentioned in the email thread for the original commit. If
reviewers think fewer tests are better, I can drop the test.
On Tue, Sep 29, 2026 at 7:25 PM Matheus Alcantara
<matheusssilv97@gmail.com> wrote:
> But I don't think that's enough. The underlying issue is that the re-add
> looks the domain up by the name saved in its definition, so the name can
> point to a different type by the time the constraint is re-added. In
> your isolation test example, if c_swap creates a new domain instead
> (CREATE DOMAIN d AS ct), the type check passes, the ALTER succeeds, and
> d_check silently ends up on the new domain, while the original (now
> d_old) loses it.
>
> Note that this isn't new with the patch. What 0003 changes is that
> without the ownership check, it also works when the domain belongs to
> someone other than the user running the ALTER.
>
> We may try to capture the domain oid above AlterDomainAddConstraint,
> while the old constraint still exists and re-add the constraint to that
> OID instead of resolving the name again. But I think that it will
> require more code to write which would make it harder for back patching.
> Looking for thoughts here.
>
Thanks for pointing this out. Perhaps this can be done in a separate
patch without back-patching.
Regards,
Nitin Motiani
Google
Attachments:
[application/x-patch] v4-0003-Don-t-require-ownership-when-rebuilding-constrain.patch (9.1K, ../../CAH5HC945XpFgKhZK9vQug5-A=_-dWRt2_LzOp9unA4JEAA941Q@mail.gmail.com/2-v4-0003-Don-t-require-ownership-when-rebuilding-constrain.patch)
download | inline diff:
From 9e9d4532fc2dd12a1ac367d0ad1d1e74cb318dcf Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:18:56 -0300
Subject: [PATCH v4 3/3] Don't require ownership when rebuilding constraints in
ALTER TABLE
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a constraint that depends on the altered column,
it drops the constraint and re-creates it from its saved definition.
For domain CHECK constraints the re-creation goes through
AlterDomainAddConstraint(), which calls checkDomainOwner(), and any
comment on a table or domain constraint is restored with
CommentObject(), which requires ownership of the constraint's table or
domain. So a user altering a type or table they own would fail with
"must be owner of type ..." or "must be owner of relation ..." if
another user's domain or table has a constraint that depends on it.
Since types grant USAGE to PUBLIC by default, any user could create
such a dependency and block the owner from altering their own type.
These checks don't make sense here: the user isn't choosing to add a
constraint or comment, just restoring ones that already existed, and
the matching drop is already done without any permission checks.
Rebuilding a table constraint without a comment also doesn't check
ownership.
Fix by skipping the ownership check in AlterDomainAddConstraint() when
is_readd is set, as we already do for the USAGE check on types used
by the expression, and by restoring comments directly with
CreateComments() instead of CommentObject().
The domain part of this dates back to af20e2d72, which added
rebuilding of domain constraints.
---
src/backend/commands/tablecmds.c | 22 +++++++++++++++-
src/backend/commands/typecmds.c | 24 ++++++++++++++----
src/test/regress/expected/domain.out | 38 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 31 +++++++++++++++++++++++
4 files changed, 109 insertions(+), 6 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cad92dd8577..a042b1fabf7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -5560,7 +5560,27 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
}
case AT_ReAddComment: /* Re-add existing comment */
- address = CommentObject((CommentStmt *) cmd->def);
+ {
+ CommentStmt *stmt = (CommentStmt *) cmd->def;
+ Relation comrel;
+
+ /*
+ * Don't use CommentObject(), since that requires ownership of
+ * the constraint's table or domain, which the user altering a
+ * column the constraint depends on need not have. We're just
+ * restoring a comment that already existed.
+ */
+ Assert(stmt->objtype == OBJECT_TABCONSTRAINT ||
+ stmt->objtype == OBJECT_DOMCONSTRAINT);
+ address = get_object_address(stmt->objtype, stmt->object,
+ &comrel,
+ ShareUpdateExclusiveLock,
+ false);
+ CreateComments(address.objectId, address.classId,
+ address.objectSubId, stmt->comment);
+ if (comrel != NULL)
+ relation_close(comrel, NoLock);
+ }
break;
case AT_AddIndexConstraint: /* ADD CONSTRAINT USING INDEX */
address = ATExecAddIndexConstraint(tab, rel, (IndexStmt *) cmd->def,
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index d0349079a1e..7e9846bda0e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3004,8 +3004,22 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
elog(ERROR, "cache lookup failed for type %u", domainoid);
typTup = (Form_pg_type) GETSTRUCT(tup);
- /* Check it's a domain and check user has permission for ALTER DOMAIN */
- checkDomainOwner(tup);
+ /*
+ * Check it's a domain and check user has permission for ALTER DOMAIN.
+ * When re-adding a constraint during ALTER TABLE, skip the permission
+ * check since the constraint already existed, and the user altering a
+ * column it depends on need not own the domain.
+ */
+ if (is_readd)
+ {
+ if (typTup->typtype != TYPTYPE_DOMAIN)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("%s is not a domain",
+ format_type_be(typTup->oid))));
+ }
+ else
+ checkDomainOwner(tup);
if (!IsA(newConstraint, Constraint))
elog(ERROR, "unrecognized node type: %d",
@@ -3034,9 +3048,9 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
* to.
*
* When re-adding a constraint during ALTER TABLE, the tables using
- * the domain might not have been rewritten to match their new
- * catalog definitions yet, so the caller must do the validation after
- * its rewrite phase instead.
+ * the domain might not have been rewritten to match their new catalog
+ * definitions yet, so the caller must do the validation after its
+ * rewrite phase instead.
*/
if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 9042ded6078..35a2e74b6ff 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -605,6 +605,44 @@ select pg_get_constraintdef(oid), convalidated from pg_constraint
drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+ conname | pg_get_constraintdef | obj_description
+-----------------+--------------------------------------------------+-----------------------
+ domrw_dt1_check | CHECK (((ROW((VALUE)::bigint)::domrw_rt).i > 0)) | domain over int
+ domrw_dt2_check | CHECK (((VALUE).i > 0)) | domain over composite
+ domrw_t_check | CHECK (((ROW((x)::bigint)::domrw_rt).i > 0)) | table constraint
+(3 rows)
+
+select (-1)::domrw_dt1; -- fail
+ERROR: value for domain domrw_dt1 violates check constraint "domrw_dt1_check"
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b791691f560..b0cfeb002df 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -330,6 +330,37 @@ drop table domrw_u;
drop domain domrw_dt;
drop type domrw_rt;
+-- Rebuilding a constraint (and its comment) owned by someone else must not
+-- require ownership of the constraint's domain or table
+create role regress_domrw_typeowner;
+create role regress_domrw_conowner;
+grant create on schema public to regress_domrw_typeowner, regress_domrw_conowner;
+set role regress_domrw_typeowner;
+create type domrw_rt as (i int);
+set role regress_domrw_conowner;
+create domain domrw_dt1 as int
+ constraint domrw_dt1_check check ((row(value)::domrw_rt).i > 0);
+comment on constraint domrw_dt1_check on domain domrw_dt1 is 'domain over int';
+create domain domrw_dt2 as domrw_rt
+ constraint domrw_dt2_check check ((value).i > 0);
+comment on constraint domrw_dt2_check on domain domrw_dt2 is 'domain over composite';
+create table domrw_t (x int
+ constraint domrw_t_check check ((row(x)::domrw_rt).i > 0));
+comment on constraint domrw_t_check on domrw_t is 'table constraint';
+set role regress_domrw_typeowner;
+alter type domrw_rt alter attribute i type bigint;
+reset role;
+select conname, pg_get_constraintdef(oid), obj_description(oid, 'pg_constraint')
+ from pg_constraint where conname like 'domrw\_%' order by conname;
+select (-1)::domrw_dt1; -- fail
+drop table domrw_t;
+drop domain domrw_dt1;
+drop domain domrw_dt2;
+drop type domrw_rt;
+revoke create on schema public from regress_domrw_typeowner, regress_domrw_conowner;
+drop role regress_domrw_typeowner;
+drop role regress_domrw_conowner;
+
-- Test domains over arrays of composite
--
2.56.0.rc1.315.gc6ed9934b7-goog
[application/x-patch] v4-0001-Fix-ALTER-.-TYPE-failure-with-dependent-domain-co.patch (7.8K, ../../CAH5HC945XpFgKhZK9vQug5-A=_-dWRt2_LzOp9unA4JEAA941Q@mail.gmail.com/3-v4-0001-Fix-ALTER-.-TYPE-failure-with-dependent-domain-co.patch)
download | inline diff:
From 934c6a653dc823d9dc328880b173994a7fb70376 Mon Sep 17 00:00:00 2001
From: Nitin Motiani <nitinmotiani@google.com>
Date: Mon, 28 Sep 2026 12:49:33 +0000
Subject: [PATCH v4 1/3] Fix ALTER ... TYPE failure with dependent domain
constraints.
When ALTER TYPE ... ALTER ATTRIBUTE ... TYPE or ALTER TABLE ... ALTER
COLUMN ... TYPE changes a column that a domain's CHECK constraint
depends on, ATPostAlterTypeCleanup() arranges for the constraint to be
dropped and re-created. Since commit af20e2d72, it chose the relation
to attach the re-creation step to by calling
get_typ_typrelid(getBaseType(con->contypid)), which only works if the
domain's base type is composite. For a domain whose base type isn't
composite (for example int, float8 or int[], possibly via another
domain) but whose CHECK expression references a composite type or a
table's row type, that returned InvalidOid, and the command failed
with "could not identify relation associated with constraint".
For a domain constraint, that relation OID only decides whether
another relation gets locked and which work queue entry receives the
AT_ReAddDomainConstraint command; the command itself identifies the
domain by name. So just use the relation being altered (tab->relid),
which is already locked.
A side effect is that for a domain over a composite type other than
the one being altered, we no longer lock that other type's relation or
add a work queue entry for it, neither of which was needed.
Bug: #19724
Reported-by: Alexander Lakhin <exclusion@gmail.com>
Author: Nitin Motiani <nitinmotiani@google.com>
Reviewed-by: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Reviewed-by: Rahul Yadav <rahul@rhyadav.com>
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
---
src/backend/commands/tablecmds.c | 12 +++--
src/test/regress/expected/domain.out | 74 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 48 ++++++++++++++++++
3 files changed, 130 insertions(+), 4 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 0274d892f2e..07cf49b286a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16141,10 +16141,14 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
relid = con->conrelid;
else
{
- /* must be a domain constraint */
- relid = get_typ_typrelid(getBaseType(con->contypid));
- if (!OidIsValid(relid))
- elog(ERROR, "could not identify relation associated with constraint %u", oldId);
+ /*
+ * Must be a domain constraint. The domain's base type need not
+ * be composite, so there may be no relation associated with it.
+ * Since the relid is only used to determine which work queue
+ * entry the command is attached to, we can use the relation which
+ * is being altered and which we already hold a lock on.
+ */
+ relid = tab->relid;
}
confrelid = con->confrelid;
conislocal = con->conislocal;
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..6f3b59b5181 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -432,6 +432,80 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
NOTICE: drop cascades to type dcomptype
+-- check altering columns used by constraints of domains whose base type
+-- isn't composite (bug #19724)
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+ERROR: operator does not exist: text > integer
+DETAIL: No operator of that name accepts the given argument types.
+HINT: You might need to add explicit type casts.
+alter type rt alter attribute i type bigint;
+select 1::dt;
+ dt
+----
+ 1
+(1 row)
+
+select (-1)::dt; -- fail
+ERROR: value for domain dt violates check constraint "dt_check"
+drop domain dt;
+drop type rt;
+-- same for a domain over float8 that uses one field of a two-field type
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+ silly
+-------
+ 1
+(1 row)
+
+select (-1.0)::silly; -- fail
+ERROR: value for domain silly violates check constraint "silly_check"
+drop domain silly;
+drop type comptype;
+-- rebuilding a constraint for one type it depends on must preserve its
+-- dependency on another
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int
+ check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type text; -- fail
+ERROR: operator does not exist: text > integer
+DETAIL: No operator of that name accepts the given argument types.
+HINT: You might need to add explicit type casts.
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+ dt_multi
+----------
+ 1
+(1 row)
+
+select (-1)::dt_multi; -- fail
+ERROR: value for domain dt_multi violates check constraint "dt_multi_check"
+drop domain dt_multi;
+drop type r1;
+drop type r2;
+-- same via ALTER TABLE, with the constraint depending on both the parent's
+-- and the inheritance child's row types
+create table dp (a int);
+create table dc (b int) inherits (dp);
+create domain dt_inh as int
+ check ((row(value)::dp).a > 0 and (row(value, value)::dc).a > 0);
+alter table dp alter column a type bigint;
+select 1::dt_inh;
+ dt_inh
+--------
+ 1
+(1 row)
+
+select (-1)::dt_inh; -- fail
+ERROR: value for domain dt_inh violates check constraint "dt_inh_check"
+drop domain dt_inh;
+drop table dc;
+drop table dp;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..f8cf8383667 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -219,6 +219,54 @@ select conname, obj_description(oid, 'pg_constraint') from pg_constraint
drop type comptype cascade;
+-- check altering columns used by constraints of domains whose base type
+-- isn't composite (bug #19724)
+create type rt as (i int);
+create domain dt as int check ((row(value)::rt).i > 0);
+alter type rt alter attribute i type text; -- fail
+alter type rt alter attribute i type bigint;
+select 1::dt;
+select (-1)::dt; -- fail
+drop domain dt;
+drop type rt;
+
+-- same for a domain over float8 that uses one field of a two-field type
+create type comptype as (r float8, i float8);
+create domain silly as float8 check ((row(value, 0)::comptype).r > 0);
+alter type comptype alter attribute r type bigint;
+select 1.0::silly;
+select (-1.0)::silly; -- fail
+drop domain silly;
+drop type comptype;
+
+-- rebuilding a constraint for one type it depends on must preserve its
+-- dependency on another
+create type r1 as (a int);
+create type r2 as (b int);
+create domain dt_multi as int
+ check ((row(value)::r1).a > 0 and (row(value)::r2).b > 0);
+alter type r1 alter attribute a type bigint;
+alter type r2 alter attribute b type text; -- fail
+alter type r2 alter attribute b type bigint;
+select 1::dt_multi;
+select (-1)::dt_multi; -- fail
+drop domain dt_multi;
+drop type r1;
+drop type r2;
+
+-- same via ALTER TABLE, with the constraint depending on both the parent's
+-- and the inheritance child's row types
+create table dp (a int);
+create table dc (b int) inherits (dp);
+create domain dt_inh as int
+ check ((row(value)::dp).a > 0 and (row(value, value)::dc).a > 0);
+alter table dp alter column a type bigint;
+select 1::dt_inh;
+select (-1)::dt_inh; -- fail
+drop domain dt_inh;
+drop table dc;
+drop table dp;
+
-- Test domains over arrays of composite
--
2.56.0.rc1.315.gc6ed9934b7-goog
[application/x-patch] v4-0002-Validate-re-added-domain-constraints-after-ALTER-.patch (14.2K, ../../CAH5HC945XpFgKhZK9vQug5-A=_-dWRt2_LzOp9unA4JEAA941Q@mail.gmail.com/4-v4-0002-Validate-re-added-domain-constraints-after-ALTER-.patch)
download | inline diff:
From b8a90f1c6a6c2a2d5730504e4969245cb3b64215 Mon Sep 17 00:00:00 2001
From: Matheus Alcantara <mths.dev@pm.me>
Date: Mon, 28 Sep 2026 15:05:36 -0300
Subject: [PATCH v4 2/3] Validate re-added domain constraints after ALTER TABLE
rewrites
When ALTER TABLE ... ALTER COLUMN TYPE (or ALTER TYPE ... ALTER
ATTRIBUTE) rebuilds a domain CHECK constraint whose expression depends
on the altered column, the constraint was re-added through
AlterDomainAddConstraint(), which validates it immediately against all
columns of the domain. That happens during Phase 2, before Phase 3 has
rewritten the affected tables, so any table that is pending a rewrite
and has a column of the domain was scanned using its new tuple
descriptor over its old heap. This could produce garbage values,
spurious "contains values that violate the new constraint" errors, or
worse, e.g. "type with OID 4294967295 does not exist" when the domain is
over a composite type.
Fix by skipping validation in AlterDomainAddConstraint() when re-adding
a constraint, and instead having ATExecCmd() remember the rebuilt
constraint so that ATRewriteTables() validates it once all tables have
been rewritten. Constraints that were NOT VALID are not validated, as
before.
This problem dates back to af20e2d72, which added rebuilding of domain
constraints, but was previously only reachable with domains over
composite types, since other domains hit the "could not identify
relation associated with constraint" error instead.
---
src/backend/commands/tablecmds.c | 59 +++++++++++++++--
src/backend/commands/typecmds.c | 11 ++--
src/include/commands/typecmds.h | 1 +
src/test/regress/expected/domain.out | 99 ++++++++++++++++++++++++++++
src/test/regress/sql/domain.sql | 63 ++++++++++++++++++
5 files changed, 224 insertions(+), 9 deletions(-)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 07cf49b286a..cad92dd8577 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -198,6 +198,8 @@ typedef struct AlteredTableInfo
bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */
char newrelpersistence; /* if above is true */
Expr *partition_constraint; /* for attach partition validation */
+ /* OIDs of re-added domain CHECK constraints to validate in Phase 3 */
+ List *domain_constraints;
/* true, if validating default due to some other attach/detach */
bool validate_default;
/* Objects to rebuild after completing ALTER TYPE operations */
@@ -5535,11 +5537,28 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
break;
case AT_ReAddDomainConstraint: /* Re-add pre-existing domain check
* constraint */
- address =
- AlterDomainAddConstraint(((AlterDomainStmt *) cmd->def)->typeName,
- ((AlterDomainStmt *) cmd->def)->def,
- NULL, true);
- break;
+ {
+ AlterDomainStmt *stmt = (AlterDomainStmt *) cmd->def;
+ Constraint *con = castNode(Constraint, stmt->def);
+ ObjectAddress constrAddr = InvalidObjectAddress;
+
+ /* only CHECK constraints can depend on a column */
+ Assert(con->contype == CONSTR_CHECK);
+
+ address = AlterDomainAddConstraint(stmt->typeName, stmt->def,
+ &constrAddr, true);
+
+ /*
+ * AlterDomainAddConstraint doesn't validate re-added
+ * constraints, since tables using the domain may not have
+ * been rewritten yet. Tell Phase 3 to do it.
+ */
+ if (!con->skip_validation)
+ tab->domain_constraints =
+ lappend_oid(tab->domain_constraints,
+ constrAddr.objectId);
+ break;
+ }
case AT_ReAddComment: /* Re-add existing comment */
address = CommentObject((CommentStmt *) cmd->def);
break;
@@ -6160,6 +6179,36 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
table_close(rel, NoLock);
}
+ /*
+ * Validate re-added domain CHECK constraints. This must wait until all
+ * tables have been rewritten, since any of them might contain columns of
+ * the domain. Don't skip relations without storage since the work queue
+ * entry might be for a standalone composite type.
+ */
+ foreach(ltab, *wqueue)
+ {
+ AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab);
+
+ foreach_oid(conoid, tab->domain_constraints)
+ {
+ HeapTuple tup;
+ Form_pg_constraint con;
+ Datum conbin;
+
+ tup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(conoid));
+ if (!HeapTupleIsValid(tup))
+ elog(ERROR, "cache lookup failed for constraint %u", conoid);
+ con = (Form_pg_constraint) GETSTRUCT(tup);
+
+ conbin = SysCacheGetAttrNotNull(CONSTROID, tup,
+ Anum_pg_constraint_conbin);
+ validateDomainCheckConstraint(con->contypid,
+ TextDatumGetCString(conbin));
+
+ ReleaseSysCache(tup);
+ }
+ }
+
/* Finally, run any afterStmts that were queued up */
foreach(ltab, *wqueue)
{
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 99a0ae2228e..d0349079a1e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -128,7 +128,6 @@ static Oid findTypeSubscriptingFunction(List *procname, Oid typeOid);
static Oid findRangeSubOpclass(List *opcname, Oid subtype);
static Oid findRangeCanonicalFunction(List *procname, Oid typeOid);
static Oid findRangeSubtypeDiffFunction(List *procname, Oid subtype);
-static void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
static void validateDomainNotNullConstraint(Oid domainoid);
static List *get_rels_with_domain(Oid domainOid, LOCKMODE lockmode);
static void checkEnumOwner(HeapTuple tup);
@@ -3029,13 +3028,17 @@ AlterDomainAddConstraint(List *names, Node *newConstraint,
constr, NameStr(typTup->typname), constrAddr,
is_readd);
-
/*
* If requested to validate the constraint, test all values stored in
* the attributes based on the domain the constraint is being added
* to.
+ *
+ * When re-adding a constraint during ALTER TABLE, the tables using
+ * the domain might not have been rewritten to match their new
+ * catalog definitions yet, so the caller must do the validation after
+ * its rewrite phase instead.
*/
- if (!constr->skip_validation)
+ if (!constr->skip_validation && !is_readd)
validateDomainCheckConstraint(domainoid, ccbin);
/*
@@ -3249,7 +3252,7 @@ validateDomainNotNullConstraint(Oid domainoid)
* Verify that all columns currently using the domain satisfy the given check
* constraint expression.
*/
-static void
+void
validateDomainCheckConstraint(Oid domainoid, const char *ccbin)
{
Expr *expr = (Expr *) stringToNode(ccbin);
diff --git a/src/include/commands/typecmds.h b/src/include/commands/typecmds.h
index 2112b4addd2..a067651f6f9 100644
--- a/src/include/commands/typecmds.h
+++ b/src/include/commands/typecmds.h
@@ -38,6 +38,7 @@ extern ObjectAddress AlterDomainAddConstraint(List *names, Node *newConstraint,
ObjectAddress *constrAddr,
bool is_readd);
extern ObjectAddress AlterDomainValidateConstraint(List *names, const char *constrName);
+extern void validateDomainCheckConstraint(Oid domainoid, const char *ccbin);
extern ObjectAddress AlterDomainDropConstraint(List *names, const char *constrName,
DropBehavior behavior, bool missing_ok);
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 6f3b59b5181..9042ded6078 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -506,6 +506,105 @@ ERROR: value for domain dt_inh violates check constraint "dt_inh_check"
drop domain dt_inh;
drop table dc;
drop table dp;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+ convalidated
+--------------
+ t
+(1 row)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+NOTICE: domain check sees value 5
+NOTICE: domain check sees value 7
+select * from domrw_t;
+ c | d
+---+-----
+ 1 | (5)
+ 2 | (7)
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_t
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+ c | d
+---+---
+ 1 | 5
+ 2 | 7
+(2 rows)
+
+drop domain domrw_dt cascade;
+NOTICE: drop cascades to column d of table domrw_ch
+drop table domrw_p cascade;
+NOTICE: drop cascades to table domrw_ch
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+ERROR: smallint out of range
+drop table domrw_u;
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+ pg_get_constraintdef | convalidated
+----------------------------------------------------------------------+--------------
+ CHECK (((ROW((VALUE)::smallint)::domrw_rt).i IS NOT NULL)) NOT VALID | f
+(1 row)
+
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
-- Test domains over arrays of composite
create type comptype as (r float8, i float8);
create domain dcomptypea as comptype[];
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index f8cf8383667..b791691f560 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -267,6 +267,69 @@ drop domain dt_inh;
drop table dc;
drop table dp;
+-- A domain constraint rebuilt by ALTER COLUMN TYPE must not be validated
+-- until tables using the domain have been rewritten. (These tests avoid
+-- ROW(value)::domrw_t, since the rebuilt expression would then contain a
+-- NULL coerced to the domain itself.)
+create function domrw_show(int) returns bool language plpgsql as
+ $$ begin raise notice 'domain check sees value %', $1; return true; end $$;
+create table domrw_t (c int);
+create domain domrw_dt as int
+ check (domrw_show(value) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, 5), (2, 7);
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+select convalidated from pg_constraint where contypid = 'domrw_dt'::regtype;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+
+-- same, domain over composite
+create type domrw_ct as (j int);
+create table domrw_t (c int);
+create domain domrw_dt as domrw_ct
+ check (domrw_show((value).j) and (null::domrw_t).c is null);
+alter table domrw_t add column d domrw_dt;
+insert into domrw_t values (1, row(5)), (2, row(7));
+alter table domrw_t alter column c type bigint; -- should see 5 and 7
+select * from domrw_t;
+drop domain domrw_dt cascade;
+drop table domrw_t;
+drop type domrw_ct;
+drop function domrw_show(int);
+
+-- domain column in an inheritance child that is rewritten by recursion
+create table domrw_p (c int);
+create domain domrw_dt as int check ((row(value)::domrw_p).c > 0);
+create table domrw_ch (d domrw_dt) inherits (domrw_p);
+insert into domrw_ch values (1, 5), (2, 7);
+alter table domrw_p alter column c type bigint;
+select * from domrw_ch;
+drop domain domrw_dt cascade;
+drop table domrw_p cascade;
+
+-- a rebuilt constraint that rejects stored values must still be enforced,
+-- even when the altered object is a standalone composite type
+create type domrw_rt as (i int);
+create domain domrw_dt as int check ((row(value)::domrw_rt).i is not null);
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter type domrw_rt alter attribute i type smallint; -- fail
+drop table domrw_u;
+
+-- a NOT VALID constraint is not validated and stays NOT VALID
+alter domain domrw_dt drop constraint domrw_dt_check;
+create table domrw_u (x domrw_dt);
+insert into domrw_u values (40000);
+alter domain domrw_dt add constraint domrw_nv
+ check ((row(value)::domrw_rt).i is not null) not valid;
+alter type domrw_rt alter attribute i type smallint;
+select pg_get_constraintdef(oid), convalidated from pg_constraint
+ where contypid = 'domrw_dt'::regtype;
+drop table domrw_u;
+drop domain domrw_dt;
+drop type domrw_rt;
+
-- Test domains over arrays of composite
--
2.56.0.rc1.315.gc6ed9934b7-goog
^ permalink raw reply [nested|flat] 11+ messages in thread
end of thread, other threads:[~2026-10-03 10:20 UTC | newest]
Thread overview: 11+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-09-28 13:29 [PATCH v1] Fix for Bug#19724 - ALTER TYPE ... ALTER ATTRIBUTE triggers internal error for base type of domain with check Nitin Motiani <nitinmotiani@google.com>
2026-09-28 14:31 ` Ayush Tiwari <ayushtiwari.slg01@gmail.com>
2026-09-28 18:45 ` Matheus Alcantara <matheusssilv97@gmail.com>
2026-09-28 20:40 ` Ayush Tiwari <ayushtiwari.slg01@gmail.com>
2026-09-28 21:06 ` Matheus Alcantara <matheusssilv97@gmail.com>
2026-09-28 23:02 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-29 07:03 ` Nitin Motiani <nitinmotiani@google.com>
2026-09-29 13:54 ` Matheus Alcantara <matheusssilv97@gmail.com>
2026-10-03 10:20 ` Nitin Motiani <nitinmotiani@google.com>
2026-09-28 16:02 ` Rahul Yadav <rahul@rhyadav.com>
2026-09-28 17:05 ` Nitin Motiani <nitinmotiani@google.com>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox