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 <pj@illuminatedcomputing.com>
Reviewed-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: jian he <jian.universality@gmail.com>
Discussion: https://www.postgresql.org/message-id/flat/CA+renyUApHgSZF9-nd-a0+OPGharLQLO=mDHcY4_qQ0+noCUVg@mail.gmail.com
This commit is contained in:
Peter Eisentraut 2024-01-24 15:43:41 +01:00
parent 74a7306310
commit 46a0cd4cef
34 changed files with 1135 additions and 52 deletions

View File

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

View File

@ -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)).

View File

@ -91,6 +91,7 @@ tests += {
'bool',
'partitions',
'stratnum',
'without_overlaps',
],
},
}

View File

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

View File

@ -2709,6 +2709,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
</para></entry>
</row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>conwithoutoverlaps</structfield> <type>bool</type>
</para>
<para>
This constraint is defined with <literal>WITHOUT OVERLAPS</literal>
(for primary keys and unique constraints).
</para></entry>
</row>
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>conkey</structfield> <type>int2[]</type>

View File

@ -298,7 +298,8 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
The optional twelfth method <function>stratnum</function> is used to
translate well-known <literal>RT*StrategyNumber</literal>s (from
<filename>src/include/access/stratnum.h</filename>) 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.
</para>
<variablelist>
@ -1185,6 +1186,17 @@ my_sortsupport(PG_FUNCTION_ARGS)
operator class has no matching strategy.
</para>
<para>
This is used for temporal index constraints (i.e., <literal>PRIMARY
KEY</literal> and <literal>UNIQUE</literal>). If the operator class
provides this function and it returns results for
<literal>RTEqualStrategyNumber</literal>, it can be used in the
non-<literal>WITHOUT OVERLAPS</literal> part(s) of an index constraint.
If it returns results for <literal>RTOverlapStrategyNumber</literal>,
the operator class can be used in the <literal>WITHOUT
OVERLAPS</literal> part of an index constraint.
</para>
<para>
The <acronym>SQL</acronym> declaration of the function must look like
this:

View File

@ -78,8 +78,8 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI
[ CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ]
{ CHECK ( <replaceable class="parameter">expression</replaceable> ) [ NO INHERIT ] |
NOT NULL <replaceable class="parameter">column_name</replaceable> [ NO INHERIT ] |
UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> |
UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
EXCLUDE [ USING <replaceable class="parameter">index_method</replaceable> ] ( <replaceable class="parameter">exclude_element</replaceable> WITH <replaceable class="parameter">operator</replaceable> [, ... ] ) <replaceable class="parameter">index_parameters</replaceable> [ WHERE ( <replaceable class="parameter">predicate</replaceable> ) ] |
FOREIGN KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) REFERENCES <replaceable class="parameter">reftable</replaceable> [ ( <replaceable class="parameter">refcolumn</replaceable> [, ... ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE <replaceable
@ -965,7 +965,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<varlistentry id="sql-createtable-parms-unique">
<term><literal>UNIQUE [ NULLS [ NOT ] DISTINCT ]</literal> (column constraint)</term>
<term><literal>UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] )</literal>
<term><literal>UNIQUE [ NULLS [ NOT ] DISTINCT ] ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] )</literal>
<optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
<listitem>
@ -979,6 +979,30 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
of these columns.
</para>
<para>
If the <literal>WITHOUT OVERLAPS</literal> 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
<literal>WITHOUT OVERLAPS</literal> 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 <literal>EXCLUDE</literal> constraint
rather than a <literal>UNIQUE</literal> constraint. So for example
<literal>UNIQUE (id, valid_at WITHOUT OVERLAPS)</literal> behaves like
<literal>EXCLUDE USING GIST (id WITH =, valid_at WITH
&amp;&amp;)</literal>. The <literal>WITHOUT OVERLAPS</literal> column
must have a range or multirange type. (Technically, any type is allowed
whose default GiST opclass includes an overlaps operator. See the
<literal>stratnum</literal> support function under <xref
linkend="gist-extensibility"/> for details.) The non-<literal>WITHOUT
OVERLAPS</literal> 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 <xref
linkend="btree-gist"/> extension (which is the expected way to use this
feature).
</para>
<para>
For the purpose of a unique constraint, null values are not
considered equal, unless <literal>NULLS NOT DISTINCT</literal> is
@ -1000,8 +1024,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
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 <literal>WITHOUT OVERLAPS</literal> clause, it
will use a GiST index.
</para>
<para>
@ -1019,7 +1045,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<varlistentry id="sql-createtable-parms-primary-key">
<term><literal>PRIMARY KEY</literal> (column constraint)</term>
<term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] )</literal>
<term><literal>PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">column_name</replaceable> WITHOUT OVERLAPS ] )</literal>
<optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
<listitem>
<para>
@ -1052,9 +1078,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</para>
<para>
Adding a <literal>PRIMARY KEY</literal> constraint will automatically
create a unique btree index on the column or group of columns used in the
constraint.
As with a <literal>UNIQUE</literal> constraint, adding a
<literal>PRIMARY KEY</literal> constraint will automatically create a
unique B-tree index, or GiST if <literal>WITHOUT OVERLAPS</literal> was
specified, on the column or group of columns used in the constraint.
</para>
<para>

View File

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

View File

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

View File

@ -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);
/*

View File

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

View File

@ -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()
*

View File

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

View File

@ -841,6 +841,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
true, /* islocal */
0, /* inhcount */
true, /* noinherit */
false, /* conwithoutoverlaps */
isInternal); /* is_internal */
}

View File

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

View File

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

View File

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

View File

@ -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 <list> rowsfrom_item rowsfrom_list opt_col_def_list
%type <boolean> opt_ordinality
%type <boolean> opt_ordinality opt_without_overlaps
%type <list> ExclusionConstraintList ExclusionConstraintElem
%type <list> func_arg_list func_arg_list_opt
%type <node> 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; }

View File

@ -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";
}
}
/*

View File

@ -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, ')');

View File

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

View File

@ -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 (");

View File

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

View File

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

View File

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

View File

@ -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 */

View File

@ -57,6 +57,6 @@
*/
/* yyyymmddN */
#define CATALOG_VERSION_NO 202401201
#define CATALOG_VERSION_NO 202401241
#endif

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -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 */

View File

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

View File

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

View File

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