Fix WITHOUT OVERLAPS' interaction with domains. master github/master
authorTom Lane <tgl@sss.pgh.pa.us>
Tue, 7 Apr 2026 18:45:33 +0000 (14:45 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Tue, 7 Apr 2026 18:45:37 +0000 (14:45 -0400)
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 <jian.universality@gmail.com>
Reviewed-by: Paul A Jungwirth <pj@illuminatedcomputing.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/CACJufxGoAmN_0iJ=hjTG0vGpOSOyy-vYyfE+-q0AWxrq2_p5XQ@mail.gmail.com
Backpatch-through: 18

src/backend/executor/execIndexing.c
src/backend/parser/parse_utilcmd.c
src/test/regress/expected/without_overlaps.out
src/test/regress/sql/without_overlaps.sql

index 4363e154c0f27376c9b2199f102f6c3c9b0cd152..eb383812901aad56f22e56453543687772745c40 100644 (file)
 #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);
        }
    }
 
index e135c91a6adfc923567d68aaea3af9a37c04d505..347f0259e5d584c3c2864b7db18717e54633593e 100644 (file)
@@ -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),
index 6f145b27c4dd9953fa758a5d18a67c746f977c01..de2f8bc4786f2d04bcbf3dfd0707d6de127dcf07 100644 (file)
@@ -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 (
index b15679d675eab697b910ac53c6db63080433b352..4833b8ac5f07be208265a69e144e64cad995099c 100644 (file)
@@ -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
 --