From 46a0cd4cefb4d9b462d8cc4df5e7ecdd190bea92 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 24 Jan 2024 15:43:41 +0100 Subject: [PATCH] Add temporal PRIMARY KEY and UNIQUE constraints Add WITHOUT OVERLAPS clause to PRIMARY KEY and UNIQUE constraints. These are backed by GiST indexes instead of B-tree indexes, since they are essentially exclusion constraints with = for the scalar parts of the key and && for the temporal part. Author: Paul A. Jungwirth Reviewed-by: Peter Eisentraut Reviewed-by: jian he Discussion: https://www.postgresql.org/message-id/flat/CA+renyUApHgSZF9-nd-a0+OPGharLQLO=mDHcY4_qQ0+noCUVg@mail.gmail.com --- contrib/btree_gist/Makefile | 2 +- .../btree_gist/expected/without_overlaps.out | 44 ++ contrib/btree_gist/meson.build | 1 + contrib/btree_gist/sql/without_overlaps.sql | 25 ++ doc/src/sgml/catalogs.sgml | 10 + doc/src/sgml/gist.sgml | 14 +- doc/src/sgml/ref/create_table.sgml | 45 +- src/backend/access/gist/gistutil.c | 29 ++ src/backend/catalog/heap.c | 2 + src/backend/catalog/index.c | 4 + src/backend/catalog/pg_constraint.c | 2 + src/backend/commands/indexcmds.c | 143 ++++++- src/backend/commands/tablecmds.c | 6 +- src/backend/commands/trigger.c | 1 + src/backend/commands/typecmds.c | 1 + src/backend/nodes/outfuncs.c | 2 + src/backend/nodes/readfuncs.c | 2 + src/backend/parser/gram.y | 29 +- src/backend/parser/parse_utilcmd.c | 28 +- src/backend/utils/adt/ruleutils.c | 2 + src/backend/utils/cache/relcache.c | 18 +- src/bin/pg_dump/pg_dump.c | 16 +- src/bin/pg_dump/pg_dump.h | 2 + src/bin/pg_dump/t/002_pg_dump.pl | 36 ++ src/bin/psql/describe.c | 12 +- src/include/access/gist.h | 3 + src/include/catalog/catversion.h | 2 +- src/include/catalog/index.h | 1 + src/include/catalog/pg_constraint.h | 10 +- src/include/commands/defrem.h | 6 +- src/include/nodes/parsenodes.h | 2 + .../regress/expected/without_overlaps.out | 395 ++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/without_overlaps.sql | 290 +++++++++++++ 34 files changed, 1135 insertions(+), 52 deletions(-) create mode 100644 contrib/btree_gist/expected/without_overlaps.out create mode 100644 contrib/btree_gist/sql/without_overlaps.sql create mode 100644 src/test/regress/expected/without_overlaps.out create mode 100644 src/test/regress/sql/without_overlaps.sql diff --git a/contrib/btree_gist/Makefile b/contrib/btree_gist/Makefile index 9ab8548bc0..7ac2df26c1 100644 --- a/contrib/btree_gist/Makefile +++ b/contrib/btree_gist/Makefile @@ -40,7 +40,7 @@ PGFILEDESC = "btree_gist - B-tree equivalent GiST operator classes" REGRESS = init int2 int4 int8 float4 float8 cash oid timestamp timestamptz \ time timetz date interval macaddr macaddr8 inet cidr text varchar char \ bytea bit varbit numeric uuid not_equal enum bool partitions \ - stratnum + stratnum without_overlaps SHLIB_LINK += $(filter -lm, $(LIBS)) diff --git a/contrib/btree_gist/expected/without_overlaps.out b/contrib/btree_gist/expected/without_overlaps.out new file mode 100644 index 0000000000..be52c522e8 --- /dev/null +++ b/contrib/btree_gist/expected/without_overlaps.out @@ -0,0 +1,44 @@ +-- Core must test WITHOUT OVERLAPS +-- with an int4range + daterange, +-- so here we do some simple tests +-- to make sure int + daterange works too, +-- since that is the expected use-case. +CREATE TABLE temporal_rng ( + id integer, + valid_at daterange, + CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng + Table "public.temporal_rng" + Column | Type | Collation | Nullable | Default +----------+-----------+-----------+----------+--------- + id | integer | | not null | + valid_at | daterange | | not null | +Indexes: + "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk'; + pg_get_constraintdef +--------------------------------------------- + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +(1 row) + +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk'; + pg_get_indexdef +------------------------------------------------------------------------------- + CREATE UNIQUE INDEX temporal_rng_pk ON temporal_rng USING gist (id, valid_at) +(1 row) + +INSERT INTO temporal_rng VALUES + (1, '[2000-01-01,2001-01-01)'); +-- same key, doesn't overlap: +INSERT INTO temporal_rng VALUES + (1, '[2001-01-01,2002-01-01)'); +-- overlaps but different key: +INSERT INTO temporal_rng VALUES + (2, '[2000-01-01,2001-01-01)'); +-- should fail: +INSERT INTO temporal_rng VALUES + (1, '[2000-06-01,2001-01-01)'); +ERROR: conflicting key value violates exclusion constraint "temporal_rng_pk" +DETAIL: Key (id, valid_at)=(1, [06-01-2000,01-01-2001)) conflicts with existing key (id, valid_at)=(1, [01-01-2000,01-01-2001)). diff --git a/contrib/btree_gist/meson.build b/contrib/btree_gist/meson.build index a44ce905e5..73b1bbf52a 100644 --- a/contrib/btree_gist/meson.build +++ b/contrib/btree_gist/meson.build @@ -91,6 +91,7 @@ tests += { 'bool', 'partitions', 'stratnum', + 'without_overlaps', ], }, } diff --git a/contrib/btree_gist/sql/without_overlaps.sql b/contrib/btree_gist/sql/without_overlaps.sql new file mode 100644 index 0000000000..433c609ab2 --- /dev/null +++ b/contrib/btree_gist/sql/without_overlaps.sql @@ -0,0 +1,25 @@ +-- Core must test WITHOUT OVERLAPS +-- with an int4range + daterange, +-- so here we do some simple tests +-- to make sure int + daterange works too, +-- since that is the expected use-case. +CREATE TABLE temporal_rng ( + id integer, + valid_at daterange, + CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk'; +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk'; + +INSERT INTO temporal_rng VALUES + (1, '[2000-01-01,2001-01-01)'); +-- same key, doesn't overlap: +INSERT INTO temporal_rng VALUES + (1, '[2001-01-01,2002-01-01)'); +-- overlaps but different key: +INSERT INTO temporal_rng VALUES + (2, '[2000-01-01,2001-01-01)'); +-- should fail: +INSERT INTO temporal_rng VALUES + (1, '[2000-06-01,2001-01-01)'); diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index c15d861e82..16b94461b2 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -2709,6 +2709,16 @@ SCRAM-SHA-256$<iteration count>:&l + + + conwithoutoverlaps bool + + + This constraint is defined with WITHOUT OVERLAPS + (for primary keys and unique constraints). + + + conkey int2[] diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml index 7c20f8467b..8a19f156d8 100644 --- a/doc/src/sgml/gist.sgml +++ b/doc/src/sgml/gist.sgml @@ -298,7 +298,8 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops); The optional twelfth method stratnum is used to translate well-known RT*StrategyNumbers (from src/include/access/stratnum.h) into strategy numbers - used by the operator class. + used by the operator class. This lets the core code look up operators for + temporal constraint indexes. @@ -1185,6 +1186,17 @@ my_sortsupport(PG_FUNCTION_ARGS) operator class has no matching strategy. + + This is used for temporal index constraints (i.e., PRIMARY + KEY and UNIQUE). If the operator class + provides this function and it returns results for + RTEqualStrategyNumber, it can be used in the + non-WITHOUT OVERLAPS part(s) of an index constraint. + If it returns results for RTOverlapStrategyNumber, + the operator class can be used in the WITHOUT + OVERLAPS part of an index constraint. + + The SQL declaration of the function must look like this: diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index e04a0692c4..079af9126a 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -78,8 +78,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI [ CONSTRAINT constraint_name ] { CHECK ( expression ) [ NO INHERIT ] | NOT NULL column_name [ NO INHERIT ] | - UNIQUE [ NULLS [ NOT ] DISTINCT ] ( column_name [, ... ] ) index_parameters | - PRIMARY KEY ( column_name [, ... ] ) index_parameters | + UNIQUE [ NULLS [ NOT ] DISTINCT ] ( column_name [, ... ] [, column_name WITHOUT OVERLAPS ] ) index_parameters | + PRIMARY KEY ( column_name [, ... ] [, column_name WITHOUT OVERLAPS ] ) index_parameters | EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ] | FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE numeric_literal, REM UNIQUE [ NULLS [ NOT ] DISTINCT ] (column constraint) - UNIQUE [ NULLS [ NOT ] DISTINCT ] ( column_name [, ... ] ) + UNIQUE [ NULLS [ NOT ] DISTINCT ] ( column_name [, ... ] [, column_name WITHOUT OVERLAPS ] ) INCLUDE ( column_name [, ...]) (table constraint) @@ -979,6 +979,30 @@ WITH ( MODULUS numeric_literal, REM of these columns. + + If the WITHOUT OVERLAPS option is specified for the + last column, then that column is checked for overlaps instead of + equality. In that case, the other columns of the constraint will allow + duplicates so long as the duplicates don't overlap in the + WITHOUT OVERLAPS column. (This is sometimes called a + temporal key, if the column is a range of dates or timestamps, but + PostgreSQL allows ranges over any base type.) In effect, such a + constraint is enforced with an EXCLUDE constraint + rather than a UNIQUE constraint. So for example + UNIQUE (id, valid_at WITHOUT OVERLAPS) behaves like + EXCLUDE USING GIST (id WITH =, valid_at WITH + &&). The WITHOUT OVERLAPS column + must have a range or multirange type. (Technically, any type is allowed + whose default GiST opclass includes an overlaps operator. See the + stratnum support function under for details.) The non-WITHOUT + OVERLAPS columns of the constraint can be any type that can be + compared for equality in a GiST index. By default, only range types are + supported, but you can use other types by adding the extension (which is the expected way to use this + feature). + + For the purpose of a unique constraint, null values are not considered equal, unless NULLS NOT DISTINCT is @@ -1000,8 +1024,10 @@ WITH ( MODULUS numeric_literal, REM - Adding a unique constraint will automatically create a unique btree - index on the column or group of columns used in the constraint. + Adding a unique constraint will automatically create a unique B-tree + index on the column or group of columns used in the constraint. But if + the constraint includes a WITHOUT OVERLAPS clause, it + will use a GiST index. @@ -1019,7 +1045,7 @@ WITH ( MODULUS numeric_literal, REM PRIMARY KEY (column constraint) - PRIMARY KEY ( column_name [, ... ] ) + PRIMARY KEY ( column_name [, ... ] [, column_name WITHOUT OVERLAPS ] ) INCLUDE ( column_name [, ...]) (table constraint) @@ -1052,9 +1078,10 @@ WITH ( MODULUS numeric_literal, REM - Adding a PRIMARY KEY constraint will automatically - create a unique btree index on the column or group of columns used in the - constraint. + As with a UNIQUE constraint, adding a + PRIMARY KEY constraint will automatically create a + unique B-tree index, or GiST if WITHOUT OVERLAPS was + specified, on the column or group of columns used in the constraint. diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c index d4d08bd118..0057044513 100644 --- a/src/backend/access/gist/gistutil.c +++ b/src/backend/access/gist/gistutil.c @@ -1070,3 +1070,32 @@ gist_stratnum_identity(PG_FUNCTION_ARGS) PG_RETURN_UINT16(strat); } + +/* + * Returns the opclass's private stratnum used for the given strategy. + * + * Calls the opclass's GIST_STRATNUM_PROC support function, if any, + * and returns the result. + * Returns InvalidStrategy if the function is not defined. + */ +StrategyNumber +GistTranslateStratnum(Oid opclass, StrategyNumber strat) +{ + Oid opfamily; + Oid opcintype; + Oid funcid; + Datum result; + + /* Look up the opclass family and input datatype. */ + if (!get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype)) + return InvalidStrategy; + + /* Check whether the function is provided. */ + funcid = get_opfamily_proc(opfamily, opcintype, opcintype, GIST_STRATNUM_PROC); + if (!OidIsValid(funcid)) + return InvalidStrategy; + + /* Ask the translation function */ + result = OidFunctionCall1Coll(funcid, InvalidOid, UInt16GetDatum(strat)); + return DatumGetUInt16(result); +} diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 45a71081d4..c73f7bcd01 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2141,6 +2141,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, is_local, /* conislocal */ inhcount, /* coninhcount */ is_no_inherit, /* connoinherit */ + false, /* conwithoutoverlaps */ is_internal); /* internally constructed? */ pfree(ccbin); @@ -2191,6 +2192,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum, is_local, inhcount, is_no_inherit, + false, /* conwithoutoverlaps */ false); return constrOid; } diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 80ba25c76f..86784efd38 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -1904,6 +1904,7 @@ index_concurrently_set_dead(Oid heapId, Oid indexId) * INDEX_CONSTR_CREATE_UPDATE_INDEX: update the pg_index row * INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS: remove existing dependencies * of index on table's columns + * INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS: constraint uses WITHOUT OVERLAPS * allow_system_table_mods: allow table to be a system catalog * is_internal: index is constructed due to internal process */ @@ -1927,11 +1928,13 @@ index_constraint_create(Relation heapRelation, bool mark_as_primary; bool islocal; bool noinherit; + bool is_without_overlaps; int inhcount; deferrable = (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) != 0; initdeferred = (constr_flags & INDEX_CONSTR_CREATE_INIT_DEFERRED) != 0; mark_as_primary = (constr_flags & INDEX_CONSTR_CREATE_MARK_AS_PRIMARY) != 0; + is_without_overlaps = (constr_flags & INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS) != 0; /* constraint creation support doesn't work while bootstrapping */ Assert(!IsBootstrapProcessingMode()); @@ -2008,6 +2011,7 @@ index_constraint_create(Relation heapRelation, islocal, inhcount, noinherit, + is_without_overlaps, is_internal); /* diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index a0232aa1bb..0a95608179 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -78,6 +78,7 @@ CreateConstraintEntry(const char *constraintName, bool conIsLocal, int conInhCount, bool conNoInherit, + bool conWithoutOverlaps, bool is_internal) { Relation conDesc; @@ -193,6 +194,7 @@ CreateConstraintEntry(const char *constraintName, values[Anum_pg_constraint_conislocal - 1] = BoolGetDatum(conIsLocal); values[Anum_pg_constraint_coninhcount - 1] = Int16GetDatum(conInhCount); values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit); + values[Anum_pg_constraint_conwithoutoverlaps - 1] = BoolGetDatum(conWithoutOverlaps); if (conkeyArray) values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray); diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 340248a3f2..7a87626f5f 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -16,6 +16,7 @@ #include "postgres.h" #include "access/amapi.h" +#include "access/gist.h" #include "access/heapam.h" #include "access/htup_details.h" #include "access/reloptions.h" @@ -86,6 +87,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo, Oid accessMethodId, bool amcanorder, bool isconstraint, + bool iswithoutoverlaps, Oid ddl_userid, int ddl_sec_context, int *ddl_save_nestlevel); @@ -144,6 +146,7 @@ typedef struct ReindexErrorInfo * to index on. * 'exclusionOpNames': list of names of exclusion-constraint operators, * or NIL if not an exclusion constraint. + * 'isWithoutOverlaps': true iff this index has a WITHOUT OVERLAPS clause. * * This is tailored to the needs of ALTER TABLE ALTER TYPE, which recreates * any indexes that depended on a changing column from their pg_get_indexdef @@ -173,7 +176,8 @@ bool CheckIndexCompatible(Oid oldId, const char *accessMethodName, const List *attributeList, - const List *exclusionOpNames) + const List *exclusionOpNames, + bool isWithoutOverlaps) { bool isconstraint; Oid *typeIds; @@ -248,8 +252,8 @@ CheckIndexCompatible(Oid oldId, coloptions, attributeList, exclusionOpNames, relationId, accessMethodName, accessMethodId, - amcanorder, isconstraint, InvalidOid, 0, NULL); - + amcanorder, isconstraint, isWithoutOverlaps, InvalidOid, + 0, NULL); /* Get the soon-obsolete pg_index tuple. */ tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId)); @@ -559,6 +563,7 @@ DefineIndex(Oid tableId, bool amcanorder; bool amissummarizing; amoptions_function amoptions; + bool exclusion; bool partitioned; bool safe_index; Datum reloptions; @@ -677,6 +682,12 @@ DefineIndex(Oid tableId, namespaceId = RelationGetNamespace(rel); + /* + * It has exclusion constraint behavior if it's an EXCLUDE constraint or a + * temporal PRIMARY KEY/UNIQUE constraint + */ + exclusion = stmt->excludeOpNames || stmt->iswithoutoverlaps; + /* Ensure that it makes sense to index this kind of relation */ switch (rel->rd_rel->relkind) { @@ -845,7 +856,7 @@ DefineIndex(Oid tableId, pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID, accessMethodId); - if (stmt->unique && !amRoutine->amcanunique) + if (stmt->unique && !stmt->iswithoutoverlaps && !amRoutine->amcanunique) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("access method \"%s\" does not support unique indexes", @@ -860,7 +871,7 @@ DefineIndex(Oid tableId, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("access method \"%s\" does not support multicolumn indexes", accessMethodName))); - if (stmt->excludeOpNames && amRoutine->amgettuple == NULL) + if (exclusion && amRoutine->amgettuple == NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("access method \"%s\" does not support exclusion constraints", @@ -913,8 +924,9 @@ DefineIndex(Oid tableId, coloptions, allIndexParams, stmt->excludeOpNames, tableId, accessMethodName, accessMethodId, - amcanorder, stmt->isconstraint, root_save_userid, - root_save_sec_context, &root_save_nestlevel); + amcanorder, stmt->isconstraint, stmt->iswithoutoverlaps, + root_save_userid, root_save_sec_context, + &root_save_nestlevel); /* * Extra checks when creating a PRIMARY KEY index. @@ -932,7 +944,7 @@ DefineIndex(Oid tableId, * We could lift this limitation if we had global indexes, but those have * their own problems, so this is a useful feature combination. */ - if (partitioned && (stmt->unique || stmt->excludeOpNames)) + if (partitioned && (stmt->unique || exclusion)) { PartitionKey key = RelationGetPartitionKey(rel); const char *constraint_type; @@ -986,10 +998,10 @@ DefineIndex(Oid tableId, * associated with index columns, too. We know what to do with * btree opclasses; if there are ever any other index types that * support unique indexes, this logic will need extension. But if - * we have an exclusion constraint, it already knows the - * operators, so we don't have to infer them. + * we have an exclusion constraint (or a temporal PK), it already + * knows the operators, so we don't have to infer them. */ - if (stmt->unique && accessMethodId != BTREE_AM_OID) + if (stmt->unique && !stmt->iswithoutoverlaps && accessMethodId != BTREE_AM_OID) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot match partition key to an index using access method \"%s\"", @@ -1028,12 +1040,12 @@ DefineIndex(Oid tableId, { Oid idx_eqop = InvalidOid; - if (stmt->unique) + if (stmt->unique && !stmt->iswithoutoverlaps) idx_eqop = get_opfamily_member(idx_opfamily, idx_opcintype, idx_opcintype, BTEqualStrategyNumber); - else if (stmt->excludeOpNames) + else if (exclusion) idx_eqop = indexInfo->ii_ExclusionOps[j]; Assert(idx_eqop); @@ -1042,7 +1054,7 @@ DefineIndex(Oid tableId, found = true; break; } - else if (stmt->excludeOpNames) + else if (exclusion) { /* * We found a match, but it's not an equality @@ -1186,6 +1198,8 @@ DefineIndex(Oid tableId, constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE; if (stmt->initdeferred) constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED; + if (stmt->iswithoutoverlaps) + constr_flags |= INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS; indexRelationId = index_create(rel, indexRelationName, indexRelationId, parentIndexId, @@ -1850,6 +1864,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo, Oid accessMethodId, bool amcanorder, bool isconstraint, + bool iswithoutoverlaps, Oid ddl_userid, int ddl_sec_context, int *ddl_save_nestlevel) @@ -1873,6 +1888,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo, else nextExclOp = NULL; + /* exclusionOpNames can be non-NIL if we are creating a partition */ + if (iswithoutoverlaps && exclusionOpNames == NIL) + { + indexInfo->ii_ExclusionOps = palloc_array(Oid, nkeycols); + indexInfo->ii_ExclusionProcs = palloc_array(Oid, nkeycols); + indexInfo->ii_ExclusionStrats = palloc_array(uint16, nkeycols); + } + if (OidIsValid(ddl_userid)) GetUserIdAndSecContext(&save_userid, &save_sec_context); @@ -2149,6 +2172,21 @@ ComputeIndexAttrs(IndexInfo *indexInfo, indexInfo->ii_ExclusionStrats[attn] = strat; nextExclOp = lnext(exclusionOpNames, nextExclOp); } + else if (iswithoutoverlaps) + { + StrategyNumber strat; + Oid opid; + + if (attn == nkeycols - 1) + strat = RTOverlapStrategyNumber; + else + strat = RTEqualStrategyNumber; + GetOperatorFromWellKnownStrategy(opclassOids[attn], atttype, + &opid, &strat); + indexInfo->ii_ExclusionOps[attn] = opid; + indexInfo->ii_ExclusionProcs[attn] = get_opcode(opid); + indexInfo->ii_ExclusionStrats[attn] = strat; + } /* * Set up the per-column options (indoption field). For now, this is @@ -2379,6 +2417,83 @@ GetDefaultOpClass(Oid type_id, Oid am_id) return InvalidOid; } +/* + * GetOperatorFromWellKnownStrategy + * + * opclass - the opclass to use + * atttype - the type to ask about + * opid - holds the operator we found + * strat - holds the input and output strategy number + * + * Finds an operator from a "well-known" strategy number. This is used for + * temporal index constraints (and other temporal features) to look up + * equality and overlaps operators, since the strategy numbers for non-btree + * indexams need not follow any fixed scheme. We ask an opclass support + * function to translate from the well-known number to the internal value. If + * the function isn't defined or it gives no result, we return + * InvalidStrategy. + */ +void +GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype, + Oid *opid, StrategyNumber *strat) +{ + Oid opfamily; + Oid opcintype; + StrategyNumber instrat = *strat; + + Assert(instrat == RTEqualStrategyNumber || instrat == RTOverlapStrategyNumber); + + *opid = InvalidOid; + + if (get_opclass_opfamily_and_input_type(opclass, &opfamily, &opcintype)) + { + /* + * Ask the opclass to translate to its internal stratnum + * + * For now we only need GiST support, but this could support other + * indexams if we wanted. + */ + *strat = GistTranslateStratnum(opclass, instrat); + if (*strat == InvalidStrategy) + { + HeapTuple tuple; + + tuple = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for operator class %u", opclass); + + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + instrat == RTEqualStrategyNumber ? + errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) : + errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)), + errdetail("Could not translate strategy number %d for operator class \"%s\" for access method \"%s\".", + instrat, NameStr(((Form_pg_opclass) GETSTRUCT(tuple))->opcname), "gist")); + + ReleaseSysCache(tuple); + } + + *opid = get_opfamily_member(opfamily, opcintype, opcintype, *strat); + } + + if (!OidIsValid(*opid)) + { + HeapTuple tuple; + + tuple = SearchSysCache1(OPFAMILYOID, ObjectIdGetDatum(opfamily)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for operator family %u", opfamily); + + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + instrat == RTEqualStrategyNumber ? + errmsg("could not identify an equality operator for type %s", format_type_be(atttype)) : + errmsg("could not identify an overlaps operator for type %s", format_type_be(atttype)), + errdetail("There is no suitable operator in operator family \"%s\" for access method \"%s\".", + NameStr(((Form_pg_opfamily) GETSTRUCT(tuple))->opfname), "gist")); + } +} + /* * makeObjectName() * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 5ccd0eec1c..eceacd4ebc 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -10340,6 +10340,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, conislocal, /* islocal */ coninhcount, /* inhcount */ connoinherit, /* conNoInherit */ + false, /* conWithoutOverlaps */ false); /* is_internal */ ObjectAddressSet(address, ConstraintRelationId, constrOid); @@ -10638,6 +10639,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, false, 1, false, + false, /* conWithoutOverlaps */ false); /* @@ -11143,6 +11145,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) false, /* islocal */ 1, /* inhcount */ false, /* conNoInherit */ + false, /* conWithoutOverlaps */ true); /* Set up partition dependencies for the new constraint */ @@ -14556,7 +14559,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt) if (CheckIndexCompatible(oldId, stmt->accessMethod, stmt->indexParams, - stmt->excludeOpNames)) + stmt->excludeOpNames, + stmt->iswithoutoverlaps)) { Relation irel = index_open(oldId, NoLock); diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 0880ca51fb..c344ff0944 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -841,6 +841,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, true, /* islocal */ 0, /* inhcount */ true, /* noinherit */ + false, /* conwithoutoverlaps */ isInternal); /* is_internal */ } diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 27cc186c16..a400fb39f6 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -3544,6 +3544,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid, true, /* is local */ 0, /* inhcount */ false, /* connoinherit */ + false, /* conwithoutoverlaps */ false); /* is_internal */ if (constrAddr) ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 296ba84518..03f67b6850 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -756,6 +756,7 @@ _outConstraint(StringInfo str, const Constraint *node) case CONSTR_PRIMARY: appendStringInfoString(str, "PRIMARY_KEY"); WRITE_NODE_FIELD(keys); + WRITE_BOOL_FIELD(without_overlaps); WRITE_NODE_FIELD(including); WRITE_NODE_FIELD(options); WRITE_STRING_FIELD(indexname); @@ -768,6 +769,7 @@ _outConstraint(StringInfo str, const Constraint *node) appendStringInfoString(str, "UNIQUE"); WRITE_BOOL_FIELD(nulls_not_distinct); WRITE_NODE_FIELD(keys); + WRITE_BOOL_FIELD(without_overlaps); WRITE_NODE_FIELD(including); WRITE_NODE_FIELD(options); WRITE_STRING_FIELD(indexname); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 1624b34581..cfb552fde7 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -427,6 +427,7 @@ _readConstraint(void) case CONSTR_PRIMARY: READ_NODE_FIELD(keys); + READ_BOOL_FIELD(without_overlaps); READ_NODE_FIELD(including); READ_NODE_FIELD(options); READ_STRING_FIELD(indexname); @@ -438,6 +439,7 @@ _readConstraint(void) case CONSTR_UNIQUE: READ_BOOL_FIELD(nulls_not_distinct); READ_NODE_FIELD(keys); + READ_BOOL_FIELD(without_overlaps); READ_NODE_FIELD(including); READ_NODE_FIELD(options); READ_STRING_FIELD(indexname); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 3460fea56b..130f7fc7c3 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -529,7 +529,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); columnref in_expr having_clause func_table xmltable array_expr OptWhereClause operator_def_arg %type rowsfrom_item rowsfrom_list opt_col_def_list -%type opt_ordinality +%type opt_ordinality opt_without_overlaps %type ExclusionConstraintList ExclusionConstraintElem %type func_arg_list func_arg_list_opt %type func_arg_expr @@ -4133,7 +4133,7 @@ ConstraintElem: n->initially_valid = true; $$ = (Node *) n; } - | UNIQUE opt_unique_null_treatment '(' columnList ')' opt_c_include opt_definition OptConsTableSpace + | UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace ConstraintAttributeSpec { Constraint *n = makeNode(Constraint); @@ -4142,11 +4142,12 @@ ConstraintElem: n->location = @1; n->nulls_not_distinct = !$2; n->keys = $4; - n->including = $6; - n->options = $7; + n->without_overlaps = $5; + n->including = $7; + n->options = $8; n->indexname = NULL; - n->indexspace = $8; - processCASbits($9, @9, "UNIQUE", + n->indexspace = $9; + processCASbits($10, @10, "UNIQUE", &n->deferrable, &n->initdeferred, NULL, NULL, yyscanner); $$ = (Node *) n; @@ -4167,7 +4168,7 @@ ConstraintElem: NULL, yyscanner); $$ = (Node *) n; } - | PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace + | PRIMARY KEY '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace ConstraintAttributeSpec { Constraint *n = makeNode(Constraint); @@ -4175,11 +4176,12 @@ ConstraintElem: n->contype = CONSTR_PRIMARY; n->location = @1; n->keys = $4; - n->including = $6; - n->options = $7; + n->without_overlaps = $5; + n->including = $7; + n->options = $8; n->indexname = NULL; - n->indexspace = $8; - processCASbits($9, @9, "PRIMARY KEY", + n->indexspace = $9; + processCASbits($10, @10, "PRIMARY KEY", &n->deferrable, &n->initdeferred, NULL, NULL, yyscanner); $$ = (Node *) n; @@ -4247,6 +4249,11 @@ opt_no_inherit: NO INHERIT { $$ = true; } | /* EMPTY */ { $$ = false; } ; +opt_without_overlaps: + WITHOUT OVERLAPS { $$ = true; } + | /*EMPTY*/ { $$ = false; } + ; + opt_column_list: '(' columnList ')' { $$ = $2; } | /*EMPTY*/ { $$ = NIL; } diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index b2d1fa9d0d..56ac4f516e 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -1716,6 +1716,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, index->unique = idxrec->indisunique; index->nulls_not_distinct = idxrec->indnullsnotdistinct; index->primary = idxrec->indisprimary; + index->iswithoutoverlaps = (idxrec->indisprimary || idxrec->indisunique) && idxrec->indisexclusion; index->transformed = true; /* don't need transformIndexStmt */ index->concurrent = false; index->if_not_exists = false; @@ -1765,7 +1766,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, int nElems; int i; - Assert(conrec->contype == CONSTRAINT_EXCLUSION); + Assert(conrec->contype == CONSTRAINT_EXCLUSION || + (index->iswithoutoverlaps && + (conrec->contype == CONSTRAINT_PRIMARY || conrec->contype == CONSTRAINT_UNIQUE))); /* Extract operator OIDs from the pg_constraint tuple */ datum = SysCacheGetAttrNotNull(CONSTROID, ht_constr, Anum_pg_constraint_conexclop); @@ -2305,6 +2308,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) } index->nulls_not_distinct = constraint->nulls_not_distinct; index->isconstraint = true; + index->iswithoutoverlaps = constraint->without_overlaps; index->deferrable = constraint->deferrable; index->initdeferred = constraint->initdeferred; @@ -2397,6 +2401,11 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) errmsg("index \"%s\" is not valid", index_name), parser_errposition(cxt->pstate, constraint->location))); + /* + * Today we forbid non-unique indexes, but we could permit GiST + * indexes whose last entry is a range type and use that to create a + * WITHOUT OVERLAPS constraint (i.e. a temporal constraint). + */ if (!index_form->indisunique) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), @@ -2673,6 +2682,23 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) notnullcmds = lappend(notnullcmds, notnullcmd); } } + + if (constraint->without_overlaps) + { + /* + * This enforces that there is at least one equality column + * besides the WITHOUT OVERLAPS columns. This is per SQL + * standard. XXX Do we need this? + */ + if (list_length(constraint->keys) < 2) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("constraint using WITHOUT OVERLAPS needs at least two columns")); + + /* WITHOUT OVERLAPS requires a GiST index */ + index->accessMethod = "gist"; + } + } /* diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 0b2a164057..b625f471a8 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2380,6 +2380,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, Anum_pg_constraint_conkey); keyatts = decompile_column_index_array(val, conForm->conrelid, &buf); + if (conForm->conwithoutoverlaps) + appendStringInfoString(&buf, " WITHOUT OVERLAPS"); appendStringInfoChar(&buf, ')'); diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 20273f0be1..5ef8f2f0ca 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -5568,11 +5568,14 @@ RelationGetIdentityKeyBitmap(Relation relation) /* * RelationGetExclusionInfo -- get info about index's exclusion constraint * - * This should be called only for an index that is known to have an - * associated exclusion constraint. It returns arrays (palloc'd in caller's - * context) of the exclusion operator OIDs, their underlying functions' - * OIDs, and their strategy numbers in the index's opclasses. We cache - * all this information since it requires a fair amount of work to get. + * This should be called only for an index that is known to have an associated + * exclusion constraint or primary key/unique constraint using WITHOUT + * OVERLAPS. + + * It returns arrays (palloc'd in caller's context) of the exclusion operator + * OIDs, their underlying functions' OIDs, and their strategy numbers in the + * index's opclasses. We cache all this information since it requires a fair + * amount of work to get. */ void RelationGetExclusionInfo(Relation indexRelation, @@ -5636,7 +5639,10 @@ RelationGetExclusionInfo(Relation indexRelation, int nelem; /* We want the exclusion constraint owning the index */ - if (conform->contype != CONSTRAINT_EXCLUSION || + if ((conform->contype != CONSTRAINT_EXCLUSION && + !(conform->conwithoutoverlaps && ( + conform->contype == CONSTRAINT_PRIMARY + || conform->contype == CONSTRAINT_UNIQUE))) || conform->conindid != RelationGetRelid(indexRelation)) continue; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index bc20a025ce..a19443becd 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -7233,6 +7233,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) i_conname, i_condeferrable, i_condeferred, + i_conwithoutoverlaps, i_contableoid, i_conoid, i_condef, @@ -7314,10 +7315,17 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) if (fout->remoteVersion >= 150000) appendPQExpBufferStr(query, - "i.indnullsnotdistinct "); + "i.indnullsnotdistinct, "); else appendPQExpBufferStr(query, - "false AS indnullsnotdistinct "); + "false AS indnullsnotdistinct, "); + + if (fout->remoteVersion >= 170000) + appendPQExpBufferStr(query, + "c.conwithoutoverlaps "); + else + appendPQExpBufferStr(query, + "NULL AS conwithoutoverlaps "); /* * The point of the messy-looking outer join is to find a constraint that @@ -7385,6 +7393,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) i_conname = PQfnumber(res, "conname"); i_condeferrable = PQfnumber(res, "condeferrable"); i_condeferred = PQfnumber(res, "condeferred"); + i_conwithoutoverlaps = PQfnumber(res, "conwithoutoverlaps"); i_contableoid = PQfnumber(res, "contableoid"); i_conoid = PQfnumber(res, "conoid"); i_condef = PQfnumber(res, "condef"); @@ -7492,6 +7501,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) constrinfo->conindex = indxinfo[j].dobj.dumpId; constrinfo->condeferrable = *(PQgetvalue(res, j, i_condeferrable)) == 't'; constrinfo->condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't'; + constrinfo->conwithoutoverlaps = *(PQgetvalue(res, j, i_conwithoutoverlaps)) == 't'; constrinfo->conislocal = true; constrinfo->separate = true; @@ -17129,6 +17139,8 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo) (k == 0) ? "" : ", ", fmtId(attname)); } + if (coninfo->conwithoutoverlaps) + appendPQExpBufferStr(q, " WITHOUT OVERLAPS"); if (indxinfo->indnkeyattrs < indxinfo->indnattrs) appendPQExpBufferStr(q, ") INCLUDE ("); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index f0772d2157..93d97a4090 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -479,6 +479,8 @@ typedef struct _constraintInfo DumpId conindex; /* identifies associated index if any */ bool condeferrable; /* true if constraint is DEFERRABLE */ bool condeferred; /* true if constraint is INITIALLY DEFERRED */ + bool conwithoutoverlaps; /* true if the constraint is WITHOUT + * OVERLAPS */ bool conislocal; /* true if constraint has local definition */ bool separate; /* true if must dump as separate item */ } ConstraintInfo; diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 3912dbf481..00b5092713 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -1015,6 +1015,42 @@ my %tests = ( }, }, + 'CONSTRAINT PRIMARY KEY / WITHOUT OVERLAPS' => { + create_sql => 'CREATE TABLE dump_test.test_table_tpk ( + col1 int4range, + col2 tstzrange, + CONSTRAINT test_table_tpk_pkey PRIMARY KEY (col1, col2 WITHOUT OVERLAPS));', + regexp => qr/^ + \QALTER TABLE ONLY dump_test.test_table_tpk\E \n^\s+ + \QADD CONSTRAINT test_table_tpk_pkey PRIMARY KEY (col1, col2 WITHOUT OVERLAPS);\E + /xm, + like => { + %full_runs, %dump_test_schema_runs, section_post_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + + 'CONSTRAINT UNIQUE / WITHOUT OVERLAPS' => { + create_sql => 'CREATE TABLE dump_test.test_table_tuq ( + col1 int4range, + col2 tstzrange, + CONSTRAINT test_table_tuq_uq UNIQUE (col1, col2 WITHOUT OVERLAPS));', + regexp => qr/^ + \QALTER TABLE ONLY dump_test.test_table_tuq\E \n^\s+ + \QADD CONSTRAINT test_table_tuq_uq UNIQUE (col1, col2 WITHOUT OVERLAPS);\E + /xm, + like => { + %full_runs, %dump_test_schema_runs, section_post_data => 1, + }, + unlike => { + exclude_dump_test_schema => 1, + only_dump_measurement => 1, + }, + }, + 'ALTER TABLE (partitioned) ADD CONSTRAINT ... FOREIGN KEY' => { create_order => 4, create_sql => 'CREATE TABLE dump_test.test_table_fk ( diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 37f9516320..9cd8783325 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -2379,6 +2379,10 @@ describeOneTableDetails(const char *schemaname, else appendPQExpBufferStr(&buf, ", false AS indisreplident"); appendPQExpBufferStr(&buf, ", c2.reltablespace"); + if (pset.sversion >= 170000) + appendPQExpBufferStr(&buf, ", con.conwithoutoverlaps"); + else + appendPQExpBufferStr(&buf, ", false AS conwithoutoverlaps"); appendPQExpBuffer(&buf, "\nFROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i\n" " LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x'))\n" @@ -2400,8 +2404,12 @@ describeOneTableDetails(const char *schemaname, printfPQExpBuffer(&buf, " \"%s\"", PQgetvalue(result, i, 0)); - /* If exclusion constraint, print the constraintdef */ - if (strcmp(PQgetvalue(result, i, 7), "x") == 0) + /* + * If exclusion constraint or PK/UNIQUE constraint WITHOUT + * OVERLAPS, print the constraintdef + */ + if (strcmp(PQgetvalue(result, i, 7), "x") == 0 || + strcmp(PQgetvalue(result, i, 12), "t") == 0) { appendPQExpBuffer(&buf, " %s", PQgetvalue(result, i, 6)); diff --git a/src/include/access/gist.h b/src/include/access/gist.h index e7ced18a5b..22dd04c141 100644 --- a/src/include/access/gist.h +++ b/src/include/access/gist.h @@ -17,6 +17,7 @@ #define GIST_H #include "access/itup.h" +#include "access/stratnum.h" #include "access/transam.h" #include "access/xlog.h" #include "access/xlogdefs.h" @@ -246,4 +247,6 @@ typedef struct do { (e).key = (k); (e).rel = (r); (e).page = (pg); \ (e).offset = (o); (e).leafkey = (l); } while (0) +extern StrategyNumber GistTranslateStratnum(Oid opclass, StrategyNumber strat); + #endif /* GIST_H */ diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 6cbe33b6fa..43d83672a6 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202401201 +#define CATALOG_VERSION_NO 202401241 #endif diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h index 99dab5940b..2ef8512dbf 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -92,6 +92,7 @@ extern Oid index_create(Relation heapRelation, #define INDEX_CONSTR_CREATE_INIT_DEFERRED (1 << 2) #define INDEX_CONSTR_CREATE_UPDATE_INDEX (1 << 3) #define INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS (1 << 4) +#define INDEX_CONSTR_CREATE_WITHOUT_OVERLAPS (1 << 5) extern Oid index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index c693091d42..01e6bc21cd 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -107,6 +107,12 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) /* Has a local definition and cannot be inherited */ bool connoinherit; + /* + * For primary keys and unique constraints, signifies the last column uses + * overlaps instead of equals. + */ + bool conwithoutoverlaps; + #ifdef CATALOG_VARLEN /* variable-length fields start here */ /* @@ -146,7 +152,8 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) /* * If an exclusion constraint, the OIDs of the exclusion operators for - * each column of the constraint + * each column of the constraint. Also set for unique constraints/primary + * keys using WITHOUT OVERLAPS. */ Oid conexclop[1] BKI_LOOKUP(pg_operator); @@ -238,6 +245,7 @@ extern Oid CreateConstraintEntry(const char *constraintName, bool conIsLocal, int conInhCount, bool conNoInherit, + bool conWithoutOverlaps, bool is_internal); extern bool ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId, diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index 29c511e319..0c53d67d3e 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -14,6 +14,7 @@ #ifndef DEFREM_H #define DEFREM_H +#include "access/stratnum.h" #include "catalog/objectaddress.h" #include "nodes/params.h" #include "parser/parse_node.h" @@ -44,10 +45,13 @@ extern char *ChooseRelationName(const char *name1, const char *name2, extern bool CheckIndexCompatible(Oid oldId, const char *accessMethodName, const List *attributeList, - const List *exclusionOpNames); + const List *exclusionOpNames, + bool isWithoutOverlaps); extern Oid GetDefaultOpClass(Oid type_id, Oid am_id); extern Oid ResolveOpClass(const List *opclass, Oid attrType, const char *accessMethodName, Oid accessMethodId); +extern void GetOperatorFromWellKnownStrategy(Oid opclass, Oid atttype, + Oid *opid, StrategyNumber *strat); /* commands/functioncmds.c */ extern ObjectAddress CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index b3181f34ae..476d55dd24 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2590,6 +2590,7 @@ typedef struct Constraint bool nulls_not_distinct; /* null treatment for UNIQUE constraints */ List *keys; /* String nodes naming referenced key * column(s); also used for NOT NULL */ + bool without_overlaps; /* WITHOUT OVERLAPS specified */ List *including; /* String nodes naming referenced nonkey * column(s) */ @@ -3219,6 +3220,7 @@ typedef struct IndexStmt bool nulls_not_distinct; /* null treatment for UNIQUE constraints */ bool primary; /* is index a primary key? */ bool isconstraint; /* is it for a pkey/unique constraint? */ + bool iswithoutoverlaps; /* is the constraint WITHOUT OVERLAPS? */ bool deferrable; /* is the constraint DEFERRABLE? */ bool initdeferred; /* is the constraint INITIALLY DEFERRED? */ bool transformed; /* true when transformIndexStmt is finished */ diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out new file mode 100644 index 0000000000..726e94102b --- /dev/null +++ b/src/test/regress/expected/without_overlaps.out @@ -0,0 +1,395 @@ +-- Tests for WITHOUT OVERLAPS. +-- +-- We leave behind several tables to test pg_dump etc: +-- temporal_rng, temporal_rng2, +-- temporal_fk_rng2rng. +-- +-- test input parser +-- +-- PK with no columns just WITHOUT OVERLAPS: +CREATE TABLE temporal_rng ( + valid_at tsrange, + CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS) +); +ERROR: constraint using WITHOUT OVERLAPS needs at least two columns +-- PK with a range column/PERIOD that isn't there: +CREATE TABLE temporal_rng ( + id INTEGER, + CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ERROR: column "valid_at" named in key does not exist +LINE 3: CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOU... + ^ +-- PK with a non-range column: +CREATE TABLE temporal_rng ( + id int4range, + valid_at TEXT, + CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ERROR: data type text has no default operator class for access method "gist" +HINT: You must specify an operator class for the index or define a default operator class for the data type. +-- PK with one column plus a range: +CREATE TABLE temporal_rng ( + -- Since we can't depend on having btree_gist here, + -- use an int4range instead of an int. + -- (The rangetypes regression test uses the same trick.) + id int4range, + valid_at tsrange, + CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng + Table "public.temporal_rng" + Column | Type | Collation | Nullable | Default +----------+-----------+-----------+----------+--------- + id | int4range | | not null | + valid_at | tsrange | | not null | +Indexes: + "temporal_rng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk'; + pg_get_constraintdef +--------------------------------------------- + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +(1 row) + +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk'; + pg_get_indexdef +------------------------------------------------------------------------------- + CREATE UNIQUE INDEX temporal_rng_pk ON temporal_rng USING gist (id, valid_at) +(1 row) + +-- PK with two columns plus a range: +-- We don't drop this table because tests below also need multiple scalar columns. +CREATE TABLE temporal_rng2 ( + id1 int4range, + id2 int4range, + valid_at tsrange, + CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng2 + Table "public.temporal_rng2" + Column | Type | Collation | Nullable | Default +----------+-----------+-----------+----------+--------- + id1 | int4range | | not null | + id2 | int4range | | not null | + valid_at | tsrange | | not null | +Indexes: + "temporal_rng2_pk" PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS) + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk'; + pg_get_constraintdef +--------------------------------------------------- + PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS) +(1 row) + +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk'; + pg_get_indexdef +--------------------------------------------------------------------------------------- + CREATE UNIQUE INDEX temporal_rng2_pk ON temporal_rng2 USING gist (id1, id2, valid_at) +(1 row) + +-- PK with a custom range type: +CREATE TYPE textrange2 AS range (subtype=text, collation="C"); +CREATE TABLE temporal_rng3 ( + id int4range, + valid_at textrange2, + CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk; +DROP TABLE temporal_rng3; +DROP TYPE textrange2; +-- PK with a multirange: +CREATE TABLE temporal_mltrng ( + id int4range, + valid_at tsmultirange, + CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +\d temporal_mltrng + Table "public.temporal_mltrng" + Column | Type | Collation | Nullable | Default +----------+--------------+-----------+----------+--------- + id | int4range | | not null | + valid_at | tsmultirange | | not null | +Indexes: + "temporal_mltrng_pk" PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) + +-- UNIQUE with no columns just WITHOUT OVERLAPS: +CREATE TABLE temporal_rng3 ( + valid_at tsrange, + CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS) +); +ERROR: constraint using WITHOUT OVERLAPS needs at least two columns +-- UNIQUE with a range column/PERIOD that isn't there: +CREATE TABLE temporal_rng3 ( + id INTEGER, + CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +ERROR: column "valid_at" named in key does not exist +LINE 3: CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OV... + ^ +-- UNIQUE with a non-range column: +CREATE TABLE temporal_rng3 ( + id int4range, + valid_at TEXT, + CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +ERROR: data type text has no default operator class for access method "gist" +HINT: You must specify an operator class for the index or define a default operator class for the data type. +-- UNIQUE with one column plus a range: +CREATE TABLE temporal_rng3 ( + id int4range, + valid_at tsrange, + CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng3 + Table "public.temporal_rng3" + Column | Type | Collation | Nullable | Default +----------+-----------+-----------+----------+--------- + id | int4range | | | + valid_at | tsrange | | | +Indexes: + "temporal_rng3_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS) + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq'; + pg_get_constraintdef +---------------------------------------- + UNIQUE (id, valid_at WITHOUT OVERLAPS) +(1 row) + +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq'; + pg_get_indexdef +--------------------------------------------------------------------------------- + CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id, valid_at) +(1 row) + +DROP TABLE temporal_rng3; +-- UNIQUE with two columns plus a range: +CREATE TABLE temporal_rng3 ( + id1 int4range, + id2 int4range, + valid_at tsrange, + CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng3 + Table "public.temporal_rng3" + Column | Type | Collation | Nullable | Default +----------+-----------+-----------+----------+--------- + id1 | int4range | | | + id2 | int4range | | | + valid_at | tsrange | | | +Indexes: + "temporal_rng3_uq" UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS) + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq'; + pg_get_constraintdef +---------------------------------------------- + UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS) +(1 row) + +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq'; + pg_get_indexdef +--------------------------------------------------------------------------------------- + CREATE UNIQUE INDEX temporal_rng3_uq ON temporal_rng3 USING gist (id1, id2, valid_at) +(1 row) + +DROP TABLE temporal_rng3; +-- UNIQUE with a custom range type: +CREATE TYPE textrange2 AS range (subtype=text, collation="C"); +CREATE TABLE temporal_rng3 ( + id int4range, + valid_at textrange2, + CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq; +DROP TABLE temporal_rng3; +DROP TYPE textrange2; +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +DROP TABLE temporal_rng; +CREATE TABLE temporal_rng ( + id int4range, + valid_at tsrange +); +ALTER TABLE temporal_rng + ADD CONSTRAINT temporal_rng_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +-- PK with USING INDEX (not possible): +CREATE TABLE temporal3 ( + id int4range, + valid_at tsrange +); +CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at); +ALTER TABLE temporal3 + ADD CONSTRAINT temporal3_pk + PRIMARY KEY USING INDEX idx_temporal3_uq; +ERROR: "idx_temporal3_uq" is not a unique index +LINE 2: ADD CONSTRAINT temporal3_pk + ^ +DETAIL: Cannot create a primary key or unique constraint using such an index. +DROP TABLE temporal3; +-- UNIQUE with USING INDEX (not possible): +CREATE TABLE temporal3 ( + id int4range, + valid_at tsrange +); +CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at); +ALTER TABLE temporal3 + ADD CONSTRAINT temporal3_uq + UNIQUE USING INDEX idx_temporal3_uq; +ERROR: "idx_temporal3_uq" is not a unique index +LINE 2: ADD CONSTRAINT temporal3_uq + ^ +DETAIL: Cannot create a primary key or unique constraint using such an index. +DROP TABLE temporal3; +-- UNIQUE with USING [UNIQUE] INDEX (possible but not a temporal constraint): +CREATE TABLE temporal3 ( + id int4range, + valid_at tsrange +); +CREATE UNIQUE INDEX idx_temporal3_uq ON temporal3 (id, valid_at); +ALTER TABLE temporal3 + ADD CONSTRAINT temporal3_uq + UNIQUE USING INDEX idx_temporal3_uq; +NOTICE: ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "idx_temporal3_uq" to "temporal3_uq" +DROP TABLE temporal3; +-- Add range column and the PK at the same time +CREATE TABLE temporal3 ( + id int4range +); +ALTER TABLE temporal3 + ADD COLUMN valid_at tsrange, + ADD CONSTRAINT temporal3_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +DROP TABLE temporal3; +-- Add range column and UNIQUE constraint at the same time +CREATE TABLE temporal3 ( + id int4range +); +ALTER TABLE temporal3 + ADD COLUMN valid_at tsrange, + ADD CONSTRAINT temporal3_uq + UNIQUE (id, valid_at WITHOUT OVERLAPS); +DROP TABLE temporal3; +-- +-- test PK inserts +-- +-- okay: +INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03')); +INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04')); +INSERT INTO temporal_rng VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05')); +INSERT INTO temporal_rng VALUES ('[3,3]', tsrange('2018-01-01', NULL)); +-- should fail: +INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05')); +ERROR: conflicting key value violates exclusion constraint "temporal_rng_pk" +DETAIL: Key (id, valid_at)=([1,2), ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")) conflicts with existing key (id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018")). +INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05')); +ERROR: null value in column "id" of relation "temporal_rng" violates not-null constraint +DETAIL: Failing row contains (null, ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018")). +INSERT INTO temporal_rng VALUES ('[3,3]', NULL); +ERROR: null value in column "valid_at" of relation "temporal_rng" violates not-null constraint +DETAIL: Failing row contains ([3,4), null). +-- +-- test a range with both a PK and a UNIQUE constraint +-- +CREATE TABLE temporal3 ( + id int4range, + valid_at daterange, + id2 int8range, + name TEXT, + CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal3 (id, valid_at, id2, name) + VALUES + ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'), + ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar') +; +DROP TABLE temporal3; +-- +-- test changing the PK's dependencies +-- +CREATE TABLE temporal3 ( + id int4range, + valid_at tsrange, + CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL; +ERROR: column "valid_at" is in a primary key +ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at)); +ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru; +ALTER TABLE temporal3 DROP COLUMN valid_thru; +DROP TABLE temporal3; +-- +-- test PARTITION BY for ranges +-- +-- temporal PRIMARY KEY: +CREATE TABLE temporal_partitioned ( + id int4range, + valid_at daterange, + name text, + CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +) PARTITION BY LIST (id); +CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]'); +CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]'); +INSERT INTO temporal_partitioned VALUES + ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'), + ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'), + ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three'); +SELECT * FROM temporal_partitioned ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [01-01-2000,02-01-2000) | one + [1,2) | [02-01-2000,03-01-2000) | one + [3,4) | [01-01-2000,01-01-2010) | three +(3 rows) + +SELECT * FROM tp1 ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [1,2) | [01-01-2000,02-01-2000) | one + [1,2) | [02-01-2000,03-01-2000) | one +(2 rows) + +SELECT * FROM tp2 ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [3,4) | [01-01-2000,01-01-2010) | three +(1 row) + +DROP TABLE temporal_partitioned; +-- temporal UNIQUE: +CREATE TABLE temporal_partitioned ( + id int4range, + valid_at daterange, + name text, + CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +) PARTITION BY LIST (id); +CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]'); +CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]'); +INSERT INTO temporal_partitioned VALUES + ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'), + ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'), + ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three'); +SELECT * FROM temporal_partitioned ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [1,2) | [01-01-2000,02-01-2000) | one + [1,2) | [02-01-2000,03-01-2000) | one + [3,4) | [01-01-2000,01-01-2010) | three +(3 rows) + +SELECT * FROM tp1 ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------ + [1,2) | [01-01-2000,02-01-2000) | one + [1,2) | [02-01-2000,03-01-2000) | one +(2 rows) + +SELECT * FROM tp2 ORDER BY id, valid_at; + id | valid_at | name +-------+-------------------------+------- + [3,4) | [01-01-2000,01-01-2010) | three +(1 row) + +DROP TABLE temporal_partitioned; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 6f5a33c234..1d8a414eea 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -78,7 +78,7 @@ test: brin_bloom brin_multi # psql depends on create_am # amutils depends on geometry, create_index_spgist, hash_index, brin # ---------- -test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role +test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role without_overlaps # collate.*.utf8 tests cannot be run in parallel with each other test: rules psql psql_crosstab amutils stats_ext collate.linux.utf8 collate.windows.win1252 diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql new file mode 100644 index 0000000000..c8e8ab994a --- /dev/null +++ b/src/test/regress/sql/without_overlaps.sql @@ -0,0 +1,290 @@ +-- Tests for WITHOUT OVERLAPS. +-- +-- We leave behind several tables to test pg_dump etc: +-- temporal_rng, temporal_rng2, +-- temporal_fk_rng2rng. + +-- +-- test input parser +-- + +-- PK with no columns just WITHOUT OVERLAPS: + +CREATE TABLE temporal_rng ( + valid_at tsrange, + CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS) +); + +-- PK with a range column/PERIOD that isn't there: + +CREATE TABLE temporal_rng ( + id INTEGER, + CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); + +-- PK with a non-range column: + +CREATE TABLE temporal_rng ( + id int4range, + valid_at TEXT, + CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); + +-- PK with one column plus a range: + +CREATE TABLE temporal_rng ( + -- Since we can't depend on having btree_gist here, + -- use an int4range instead of an int. + -- (The rangetypes regression test uses the same trick.) + id int4range, + valid_at tsrange, + CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng_pk'; +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng_pk'; + +-- PK with two columns plus a range: +-- We don't drop this table because tests below also need multiple scalar columns. +CREATE TABLE temporal_rng2 ( + id1 int4range, + id2 int4range, + valid_at tsrange, + CONSTRAINT temporal_rng2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng2 +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_pk'; +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_pk'; + + +-- PK with a custom range type: +CREATE TYPE textrange2 AS range (subtype=text, collation="C"); +CREATE TABLE temporal_rng3 ( + id int4range, + valid_at textrange2, + CONSTRAINT temporal_rng3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_pk; +DROP TABLE temporal_rng3; +DROP TYPE textrange2; + +-- PK with a multirange: +CREATE TABLE temporal_mltrng ( + id int4range, + valid_at tsmultirange, + CONSTRAINT temporal_mltrng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +\d temporal_mltrng + +-- UNIQUE with no columns just WITHOUT OVERLAPS: + +CREATE TABLE temporal_rng3 ( + valid_at tsrange, + CONSTRAINT temporal_rng3_uq UNIQUE (valid_at WITHOUT OVERLAPS) +); + +-- UNIQUE with a range column/PERIOD that isn't there: + +CREATE TABLE temporal_rng3 ( + id INTEGER, + CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); + +-- UNIQUE with a non-range column: + +CREATE TABLE temporal_rng3 ( + id int4range, + valid_at TEXT, + CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); + +-- UNIQUE with one column plus a range: + +CREATE TABLE temporal_rng3 ( + id int4range, + valid_at tsrange, + CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng3 +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq'; +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq'; +DROP TABLE temporal_rng3; + +-- UNIQUE with two columns plus a range: +CREATE TABLE temporal_rng3 ( + id1 int4range, + id2 int4range, + valid_at tsrange, + CONSTRAINT temporal_rng3_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS) +); +\d temporal_rng3 +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng3_uq'; +SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng3_uq'; +DROP TABLE temporal_rng3; + +-- UNIQUE with a custom range type: +CREATE TYPE textrange2 AS range (subtype=text, collation="C"); +CREATE TABLE temporal_rng3 ( + id int4range, + valid_at textrange2, + CONSTRAINT temporal_rng3_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE temporal_rng3 DROP CONSTRAINT temporal_rng3_uq; +DROP TABLE temporal_rng3; +DROP TYPE textrange2; + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +DROP TABLE temporal_rng; +CREATE TABLE temporal_rng ( + id int4range, + valid_at tsrange +); +ALTER TABLE temporal_rng + ADD CONSTRAINT temporal_rng_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); + +-- PK with USING INDEX (not possible): +CREATE TABLE temporal3 ( + id int4range, + valid_at tsrange +); +CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at); +ALTER TABLE temporal3 + ADD CONSTRAINT temporal3_pk + PRIMARY KEY USING INDEX idx_temporal3_uq; +DROP TABLE temporal3; + +-- UNIQUE with USING INDEX (not possible): +CREATE TABLE temporal3 ( + id int4range, + valid_at tsrange +); +CREATE INDEX idx_temporal3_uq ON temporal3 USING gist (id, valid_at); +ALTER TABLE temporal3 + ADD CONSTRAINT temporal3_uq + UNIQUE USING INDEX idx_temporal3_uq; +DROP TABLE temporal3; + +-- UNIQUE with USING [UNIQUE] INDEX (possible but not a temporal constraint): +CREATE TABLE temporal3 ( + id int4range, + valid_at tsrange +); +CREATE UNIQUE INDEX idx_temporal3_uq ON temporal3 (id, valid_at); +ALTER TABLE temporal3 + ADD CONSTRAINT temporal3_uq + UNIQUE USING INDEX idx_temporal3_uq; +DROP TABLE temporal3; + +-- Add range column and the PK at the same time +CREATE TABLE temporal3 ( + id int4range +); +ALTER TABLE temporal3 + ADD COLUMN valid_at tsrange, + ADD CONSTRAINT temporal3_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +DROP TABLE temporal3; + +-- Add range column and UNIQUE constraint at the same time +CREATE TABLE temporal3 ( + id int4range +); +ALTER TABLE temporal3 + ADD COLUMN valid_at tsrange, + ADD CONSTRAINT temporal3_uq + UNIQUE (id, valid_at WITHOUT OVERLAPS); +DROP TABLE temporal3; + +-- +-- test PK inserts +-- + +-- okay: +INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03')); +INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04')); +INSERT INTO temporal_rng VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05')); +INSERT INTO temporal_rng VALUES ('[3,3]', tsrange('2018-01-01', NULL)); + +-- should fail: +INSERT INTO temporal_rng VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05')); +INSERT INTO temporal_rng VALUES (NULL, tsrange('2018-01-01', '2018-01-05')); +INSERT INTO temporal_rng VALUES ('[3,3]', NULL); + +-- +-- test a range with both a PK and a UNIQUE constraint +-- + +CREATE TABLE temporal3 ( + id int4range, + valid_at daterange, + id2 int8range, + name TEXT, + CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT temporal3_uniq UNIQUE (id2, valid_at WITHOUT OVERLAPS) +); +INSERT INTO temporal3 (id, valid_at, id2, name) + VALUES + ('[1,1]', daterange('2000-01-01', '2010-01-01'), '[7,7]', 'foo'), + ('[2,2]', daterange('2000-01-01', '2010-01-01'), '[9,9]', 'bar') +; +DROP TABLE temporal3; + +-- +-- test changing the PK's dependencies +-- + +CREATE TABLE temporal3 ( + id int4range, + valid_at tsrange, + CONSTRAINT temporal3_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); + +ALTER TABLE temporal3 ALTER COLUMN valid_at DROP NOT NULL; +ALTER TABLE temporal3 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at)); +ALTER TABLE temporal3 RENAME COLUMN valid_at TO valid_thru; +ALTER TABLE temporal3 DROP COLUMN valid_thru; +DROP TABLE temporal3; + +-- +-- test PARTITION BY for ranges +-- + +-- temporal PRIMARY KEY: +CREATE TABLE temporal_partitioned ( + id int4range, + valid_at daterange, + name text, + CONSTRAINT temporal_paritioned_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +) PARTITION BY LIST (id); +CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]'); +CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]'); +INSERT INTO temporal_partitioned VALUES + ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'), + ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'), + ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three'); +SELECT * FROM temporal_partitioned ORDER BY id, valid_at; +SELECT * FROM tp1 ORDER BY id, valid_at; +SELECT * FROM tp2 ORDER BY id, valid_at; +DROP TABLE temporal_partitioned; + +-- temporal UNIQUE: +CREATE TABLE temporal_partitioned ( + id int4range, + valid_at daterange, + name text, + CONSTRAINT temporal_paritioned_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +) PARTITION BY LIST (id); +CREATE TABLE tp1 PARTITION OF temporal_partitioned FOR VALUES IN ('[1,1]', '[2,2]'); +CREATE TABLE tp2 PARTITION OF temporal_partitioned FOR VALUES IN ('[3,3]', '[4,4]'); +INSERT INTO temporal_partitioned VALUES + ('[1,1]', daterange('2000-01-01', '2000-02-01'), 'one'), + ('[1,1]', daterange('2000-02-01', '2000-03-01'), 'one'), + ('[3,3]', daterange('2000-01-01', '2010-01-01'), 'three'); +SELECT * FROM temporal_partitioned ORDER BY id, valid_at; +SELECT * FROM tp1 ORDER BY id, valid_at; +SELECT * FROM tp2 ORDER BY id, valid_at; +DROP TABLE temporal_partitioned;