From 4edd6036d69ce42ac1af236f659f20daed65c8d4 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 7 Apr 2026 14:45:33 -0400 Subject: [PATCH] Fix WITHOUT OVERLAPS' interaction with domains. UNIQUE/PRIMARY KEY ... WITHOUT OVERLAPS requires the no-overlap column to be a range or multirange, but it should allow a domain over such a type too. This requires minor adjustments in both the parser and executor. In passing, fix a nearby break-instead-of-continue thinko in transformIndexConstraint. This had the effect of disabling parse-time validation of the no-overlap column's type in the context of ALTER TABLE ADD CONSTRAINT, if it follows a dropped column. We'd still complain appropriately at runtime though. Author: Jian He Reviewed-by: Paul A Jungwirth Reviewed-by: Tom Lane Discussion: https://postgr.es/m/CACJufxGoAmN_0iJ=hjTG0vGpOSOyy-vYyfE+-q0AWxrq2_p5XQ@mail.gmail.com Backpatch-through: 18 --- src/backend/executor/execIndexing.c | 12 +++++- src/backend/parser/parse_utilcmd.c | 17 +++++--- .../regress/expected/without_overlaps.out | 39 +++++++++++++++++++ src/test/regress/sql/without_overlaps.sql | 31 +++++++++++++++ 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/src/backend/executor/execIndexing.c b/src/backend/executor/execIndexing.c index 4363e154c0f..eb383812901 100644 --- a/src/backend/executor/execIndexing.c +++ b/src/backend/executor/execIndexing.c @@ -115,6 +115,7 @@ #include "nodes/nodeFuncs.h" #include "storage/lmgr.h" #include "utils/injection_point.h" +#include "utils/lsyscache.h" #include "utils/multirangetypes.h" #include "utils/rangetypes.h" #include "utils/snapmgr.h" @@ -753,11 +754,18 @@ check_exclusion_or_unique_constraint(Relation heap, Relation index, { TupleDesc tupdesc = RelationGetDescr(heap); Form_pg_attribute att = TupleDescAttr(tupdesc, attno - 1); - TypeCacheEntry *typcache = lookup_type_cache(att->atttypid, 0); + TypeCacheEntry *typcache = lookup_type_cache(att->atttypid, + TYPECACHE_DOMAIN_BASE_INFO); + char typtype; + + if (OidIsValid(typcache->domainBaseType)) + typtype = get_typtype(typcache->domainBaseType); + else + typtype = typcache->typtype; ExecWithoutOverlapsNotEmpty(heap, att->attname, values[indnkeyatts - 1], - typcache->typtype, att->atttypid); + typtype, att->atttypid); } } diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index e135c91a6ad..347f0259e5d 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -2760,7 +2760,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) /* * The WITHOUT OVERLAPS part (if any) must be a range or - * multirange type. + * multirange type, or a domain over such a type. */ if (constraint->without_overlaps && lc == list_last_cell(constraint->keys)) { @@ -2778,8 +2778,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) const char *attname; if (attr->attisdropped) - break; - + continue; attname = NameStr(attr->attname); if (strcmp(attname, key) == 0) { @@ -2791,10 +2790,16 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) } if (found) { + /* Look up column type if we didn't already */ if (!OidIsValid(typid) && column) - typid = typenameTypeId(NULL, column->typeName); - - if (!OidIsValid(typid) || !(type_is_range(typid) || type_is_multirange(typid))) + typid = typenameTypeId(cxt->pstate, + column->typeName); + /* Look through any domain */ + if (OidIsValid(typid)) + typid = getBaseType(typid); + /* Complain if not range/multirange */ + if (!OidIsValid(typid) || + !(type_is_range(typid) || type_is_multirange(typid))) ereport(ERROR, (errcode(ERRCODE_DATATYPE_MISMATCH), errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range or multirange type", key), diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out index 6f145b27c4d..de2f8bc4786 100644 --- a/src/test/regress/expected/without_overlaps.out +++ b/src/test/regress/expected/without_overlaps.out @@ -314,6 +314,45 @@ ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq; DROP TABLE temporal_rng3; DROP TYPE textrange2; -- +-- test PRIMARY KEY and UNIQUE constraints' interaction with domains +-- +-- range over domain: +CREATE DOMAIN int4_d as integer check (value <> 10); +CREATE TYPE int4_d_range as range (subtype = int4_d); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4_d_range, + CONSTRAINT temporal_rng4_pk PRIMARY KEY(id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,11)', '[9,10)'); -- start bound violates domain +ERROR: value for domain int4_d violates check constraint "int4_d_check" +LINE 1: INSERT INTO temporal_rng4 VALUES ('[1,11)', '[9,10)'); + ^ +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- end bound violates domain +ERROR: value for domain int4_d violates check constraint "int4_d_check" +LINE 1: INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); + ^ +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,5)'); -- overlaps +ERROR: conflicting key value violates exclusion constraint "temporal_rng4_pk" +DETAIL: Key (id, valid_at)=([1,2), [2,5)) conflicts with existing key (id, valid_at)=([1,2), [1,13)). +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[30,)'); -- null bound is okay +DROP TABLE temporal_rng4; +-- domain over range: +CREATE DOMAIN int4range_d AS int4range CHECK (VALUE <> '[10,11)'); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4range_d, + CONSTRAINT temporal_rng4_pk UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- violates domain +ERROR: value for domain int4range_d violates check constraint "int4range_d_check" +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,13)'); -- overlaps +ERROR: conflicting key value violates exclusion constraint "temporal_rng4_pk" +DETAIL: Key (id, valid_at)=([1,2), [2,13)) conflicts with existing key (id, valid_at)=([1,2), [1,13)). +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +DROP TABLE temporal_rng4; +-- -- test ALTER TABLE ADD CONSTRAINT -- CREATE TABLE temporal_rng ( diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql index b15679d675e..4833b8ac5f0 100644 --- a/src/test/regress/sql/without_overlaps.sql +++ b/src/test/regress/sql/without_overlaps.sql @@ -180,6 +180,37 @@ ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq; DROP TABLE temporal_rng3; DROP TYPE textrange2; +-- +-- test PRIMARY KEY and UNIQUE constraints' interaction with domains +-- + +-- range over domain: +CREATE DOMAIN int4_d as integer check (value <> 10); +CREATE TYPE int4_d_range as range (subtype = int4_d); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4_d_range, + CONSTRAINT temporal_rng4_pk PRIMARY KEY(id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,11)', '[9,10)'); -- start bound violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- end bound violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,5)'); -- overlaps +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[30,)'); -- null bound is okay +DROP TABLE temporal_rng4; + +-- domain over range: +CREATE DOMAIN int4range_d AS int4range CHECK (VALUE <> '[10,11)'); +CREATE TABLE temporal_rng4 ( + id int4range, + valid_at int4range_d, + CONSTRAINT temporal_rng4_pk UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[10,11)'); -- violates domain +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[2,13)'); -- overlaps +INSERT INTO temporal_rng4 VALUES ('[1,2)', '[1,13)'), ('[1,2)', '[20,23)'); -- okay +DROP TABLE temporal_rng4; + -- -- test ALTER TABLE ADD CONSTRAINT --