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 <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
This commit is contained in:
Tom Lane 2026-04-07 14:45:33 -04:00
parent 294520c444
commit 4edd6036d6
4 changed files with 91 additions and 8 deletions

View file

@ -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);
}
}

View 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),

View 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 (

View 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
--