v16-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch
text/x-patch
Filename: v16-0001-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch
Type: text/x-patch
Part: 0
Message:
Re: SQL:2011 application time
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v16-0001
Subject: Add temporal PRIMARY KEY and UNIQUE constraints
| File | + | − |
|---|---|---|
| doc/src/sgml/catalogs.sgml | 12 | 0 |
| doc/src/sgml/ref/create_table.sgml | 39 | 6 |
| src/backend/catalog/heap.c | 2 | 0 |
| src/backend/catalog/index.c | 6 | 2 |
| src/backend/catalog/pg_constraint.c | 2 | 0 |
| src/backend/commands/indexcmds.c | 141 | 14 |
| src/backend/commands/tablecmds.c | 11 | 3 |
| src/backend/commands/trigger.c | 1 | 0 |
| src/backend/commands/typecmds.c | 1 | 0 |
| src/backend/parser/gram.y | 19 | 11 |
| src/backend/parser/parse_utilcmd.c | 123 | 1 |
| src/backend/utils/adt/ruleutils.c | 20 | 9 |
| src/backend/utils/cache/relcache.c | 7 | 3 |
| src/bin/pg_dump/pg_dump.c | 29 | 6 |
| src/bin/pg_dump/pg_dump.h | 1 | 0 |
| src/bin/pg_dump/t/002_pg_dump.pl | 46 | 0 |
| src/bin/psql/describe.c | 10 | 2 |
| src/include/catalog/index.h | 1 | 0 |
| src/include/catalog/pg_constraint.h | 8 | 1 |
| src/include/commands/defrem.h | 2 | 1 |
| src/include/nodes/parsenodes.h | 4 | 0 |
| src/test/regress/expected/without_overlaps.out | 359 | 0 |
| src/test/regress/parallel_schedule | 1 | 1 |
| src/test/regress/sql/without_overlaps.sql | 262 | 0 |
From afc16eb5fb360f5c7f5229eea18c0867098f9f89 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Mon, 28 Jun 2021 17:33:27 -0700
Subject: [PATCH v16 1/5] Add temporal PRIMARY KEY and UNIQUE constraints
- Added WITHOUT OVERLAPS to the bison grammar. We permit only range
columns, not yet PERIODs (which we'll add soon).
- Temporal PRIMARY KEYs and UNIQUE constraints 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.
- Added pg_constraint.contemporal to say whether a constraint is a
temporal constraint.
- Added docs and tests.
- Added pg_dump support.
---
doc/src/sgml/catalogs.sgml | 12 +
doc/src/sgml/ref/create_table.sgml | 45 ++-
src/backend/catalog/heap.c | 2 +
src/backend/catalog/index.c | 8 +-
src/backend/catalog/pg_constraint.c | 2 +
src/backend/commands/indexcmds.c | 155 +++++++-
src/backend/commands/tablecmds.c | 14 +-
src/backend/commands/trigger.c | 1 +
src/backend/commands/typecmds.c | 1 +
src/backend/parser/gram.y | 30 +-
src/backend/parser/parse_utilcmd.c | 124 +++++-
src/backend/utils/adt/ruleutils.c | 29 +-
src/backend/utils/cache/relcache.c | 10 +-
src/bin/pg_dump/pg_dump.c | 35 +-
src/bin/pg_dump/pg_dump.h | 1 +
src/bin/pg_dump/t/002_pg_dump.pl | 46 +++
src/bin/psql/describe.c | 12 +-
src/include/catalog/index.h | 1 +
src/include/catalog/pg_constraint.h | 9 +-
src/include/commands/defrem.h | 3 +-
src/include/nodes/parsenodes.h | 4 +
.../regress/expected/without_overlaps.out | 359 ++++++++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/without_overlaps.sql | 262 +++++++++++++
24 files changed, 1107 insertions(+), 60 deletions(-)
create mode 100644 src/test/regress/expected/without_overlaps.out
create mode 100644 src/test/regress/sql/without_overlaps.sql
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e09adb45e4..312e00c90a 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2709,6 +2709,18 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>contemporal</structfield> <type>bool</type>
+ </para>
+ <para>
+ This constraint is a application-time temporal constraint,
+ defined with <literal>WITHOUT OVERLAPS</literal> (for primary
+ key and unique constraints) or <literal>PERIOD</literal> (for
+ foreign keys).
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>conkey</structfield> <type>int2[]</type>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e04a0692c4..ad5bf21453 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 <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">temporal_interval</replaceable> WITHOUT OVERLAPS ] ) <replaceable class="parameter">index_parameters</replaceable> |
+ PRIMARY KEY ( <replaceable class="parameter">column_name</replaceable> [, ... ] [, <replaceable class="parameter">temporal_interval</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
@@ -107,6 +107,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
{ <replaceable class="parameter">column_name</replaceable> | ( <replaceable class="parameter">expression</replaceable> ) } [ <replaceable class="parameter">opclass</replaceable> ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ]
+<phrase><replaceable class="parameter">temporal_interval</replaceable> in a <literal>PRIMARY KEY</literal>, <literal>UNIQUE</literal>, or <literal>FOREIGN KEY</literal> constraint is:</phrase>
+
+<replaceable class="parameter">range_column_name</replaceable>
+
<phrase><replaceable class="parameter">referential_action</replaceable> in a <literal>FOREIGN KEY</literal>/<literal>REFERENCES</literal> constraint is:</phrase>
{ NO ACTION | RESTRICT | CASCADE | SET NULL [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] | SET DEFAULT [ ( <replaceable class="parameter">column_name</replaceable> [, ... ] ) ] }
@@ -1001,7 +1005,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<para>
Adding a unique constraint will automatically create a unique btree
- index on the column or group of columns used in the constraint.
+ 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 and behave like a temporal <literal>PRIMARY
+ KEY</literal>: preventing duplicates only in overlapping time periods.
</para>
<para>
@@ -1019,7 +1026,8 @@ 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">temporal_interval</replaceable> WITHOUT OVERLAPS ] )</literal>
<optional> <literal>INCLUDE ( <replaceable class="parameter">column_name</replaceable> [, ...])</literal> </optional> (table constraint)</term>
<listitem>
<para>
@@ -1053,8 +1061,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
<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.
+ create a unique btree index (or GiST if temporal) on the column or group of
+ columns used in the constraint.
</para>
<para>
@@ -1067,6 +1075,31 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
(e.g., <literal>DROP COLUMN</literal>) can cause cascaded constraint and
index deletion.
</para>
+
+ <para>
+ A <literal>PRIMARY KEY</literal> with a <literal>WITHOUT OVERLAPS</literal> option
+ is a <emphasis>temporal</emphasis> primary key.
+ The <literal>WITHOUT OVERLAPS</literal> column
+ must be a range type or a <literal>PERIOD</literal>
+ and is used to constrain the record's applicability
+ to just that range/period (usually a range of dates or timestamps,
+ although Postgres allows a range/period over any base type).
+ </para>
+
+ <para>
+ The non-<literal>WITHOUT OVERLAPS</literal> part(s) of the key 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).
+ These ordinary parts of the primary key do allow duplicates,
+ so long as the duplicates don't overlap in the
+ <literal>WITHOUT OVERLAPS</literal> column.
+ In effect a temporal <literal>PRIMARY KEY</literal> is enforced with an
+ <literal>EXCLUDE</literal> constraint rather than a <literal>UNIQUE</literal>
+ constraint. So for example <literal>PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)</literal>
+ behaves like <literal>EXCLUDE USING GIST (id WITH =, valid_at WITH &&)</literal>.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index b42711f574..7148dd1787 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2140,6 +2140,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
is_local, /* conislocal */
inhcount, /* coninhcount */
is_no_inherit, /* connoinherit */
+ false, /* contemporal */
is_internal); /* internally constructed? */
pfree(ccbin);
@@ -2190,6 +2191,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
is_local,
inhcount,
is_no_inherit,
+ false, /* contemporal */
false);
return constrOid;
}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 72f476b51d..1a617b042d 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -255,8 +255,8 @@ index_check_primary_key(Relation heapRel,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("primary keys cannot be expressions")));
- /* System attributes are never null, so no need to check */
- if (attnum < 0)
+ /* System attributes are never null, so no need to check. */
+ if (attnum <= 0)
continue;
atttuple = SearchSysCache2(ATTNUM,
@@ -1897,6 +1897,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_TEMPORAL: constraint is for a temporal primary key
* allow_system_table_mods: allow table to be a system catalog
* is_internal: index is constructed due to internal process
*/
@@ -1920,11 +1921,13 @@ index_constraint_create(Relation heapRelation,
bool mark_as_primary;
bool islocal;
bool noinherit;
+ bool is_temporal;
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_temporal = (constr_flags & INDEX_CONSTR_CREATE_TEMPORAL) != 0;
/* constraint creation support doesn't work while bootstrapping */
Assert(!IsBootstrapProcessingMode());
@@ -2001,6 +2004,7 @@ index_constraint_create(Relation heapRelation,
islocal,
inhcount,
noinherit,
+ is_temporal, /* contemporal */
is_internal);
/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index e9d4d6006e..d5329ca8c9 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 conTemporal,
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_contemporal - 1] = BoolGetDatum(conTemporal);
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 ab8b81b302..794bde190e 100644
--- a/src/backend/commands/indexcmds.c
+++ b/src/backend/commands/indexcmds.c
@@ -73,6 +73,11 @@
/* non-export function prototypes */
static bool CompareOpclassOptions(const Datum *opts1, const Datum *opts2, int natts);
static void CheckPredicate(Expr *predicate);
+static void get_index_attr_temporal_operator(Oid opclass,
+ Oid atttype,
+ bool isoverlaps,
+ Oid *opid,
+ int *strat);
static void ComputeIndexAttrs(IndexInfo *indexInfo,
Oid *typeOids,
Oid *collationOids,
@@ -85,6 +90,7 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo,
Oid accessMethodId,
bool amcanorder,
bool isconstraint,
+ bool istemporal,
Oid ddl_userid,
int ddl_sec_context,
int *ddl_save_nestlevel);
@@ -142,6 +148,7 @@ typedef struct ReindexErrorInfo
* to index on.
* 'exclusionOpNames': list of names of exclusion-constraint operators,
* or NIL if not an exclusion constraint.
+ * 'istemporal': 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
@@ -171,7 +178,8 @@ bool
CheckIndexCompatible(Oid oldId,
const char *accessMethodName,
const List *attributeList,
- const List *exclusionOpNames)
+ const List *exclusionOpNames,
+ bool istemporal)
{
bool isconstraint;
Oid *typeIds;
@@ -244,8 +252,8 @@ CheckIndexCompatible(Oid oldId,
coloptions, attributeList,
exclusionOpNames, relationId,
accessMethodName, accessMethodId,
- amcanorder, isconstraint, InvalidOid, 0, NULL);
-
+ amcanorder, isconstraint, istemporal, InvalidOid,
+ 0, NULL);
/* Get the soon-obsolete pg_index tuple. */
tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId));
@@ -553,6 +561,7 @@ DefineIndex(Oid tableId,
bool amcanorder;
bool amissummarizing;
amoptions_function amoptions;
+ bool exclusion;
bool partitioned;
bool safe_index;
Datum reloptions;
@@ -671,6 +680,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->istemporal;
+
/* Ensure that it makes sense to index this kind of relation */
switch (rel->rd_rel->relkind)
{
@@ -839,7 +854,7 @@ DefineIndex(Oid tableId,
pgstat_progress_update_param(PROGRESS_CREATEIDX_ACCESS_METHOD_OID,
accessMethodId);
- if (stmt->unique && !amRoutine->amcanunique)
+ if (stmt->unique && !stmt->istemporal && !amRoutine->amcanunique)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("access method \"%s\" does not support unique indexes",
@@ -854,7 +869,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",
@@ -906,8 +921,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->istemporal,
+ root_save_userid, root_save_sec_context,
+ &root_save_nestlevel);
/*
* Extra checks when creating a PRIMARY KEY index.
@@ -925,7 +941,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;
@@ -933,6 +949,8 @@ DefineIndex(Oid tableId,
if (stmt->primary)
constraint_type = "PRIMARY KEY";
+ else if (stmt->unique && stmt->istemporal)
+ constraint_type = "temporal UNIQUE";
else if (stmt->unique)
constraint_type = "UNIQUE";
else if (stmt->excludeOpNames)
@@ -979,10 +997,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->istemporal && accessMethodId != BTREE_AM_OID)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot match partition key to an index using access method \"%s\"",
@@ -1015,12 +1033,12 @@ DefineIndex(Oid tableId,
{
Oid idx_eqop = InvalidOid;
- if (stmt->unique)
+ if (stmt->unique && !stmt->istemporal)
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);
@@ -1029,7 +1047,7 @@ DefineIndex(Oid tableId,
found = true;
break;
}
- else if (stmt->excludeOpNames)
+ else if (exclusion)
{
/*
* We found a match, but it's not an equality
@@ -1173,6 +1191,8 @@ DefineIndex(Oid tableId,
constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE;
if (stmt->initdeferred)
constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED;
+ if (stmt->istemporal)
+ constr_flags |= INDEX_CONSTR_CREATE_TEMPORAL;
indexRelationId =
index_create(rel, indexRelationName, indexRelationId, parentIndexId,
@@ -1841,6 +1861,91 @@ CheckPredicate(Expr *predicate)
errmsg("functions in index predicate must be marked IMMUTABLE")));
}
+/*
+ * get_index_attr_temporal_operator
+ *
+ * Finds an operator for a temporal index attribute.
+ * We need an equality operator for normal keys
+ * and an overlaps operator for the range.
+ * Returns the operator oid and strategy in opid and strat,
+ * respectively.
+ */
+static void
+get_index_attr_temporal_operator(Oid opclass,
+ Oid atttype,
+ bool isoverlaps,
+ Oid *opid,
+ int *strat)
+{
+ Oid opfamily;
+ const char *opname;
+
+ opfamily = get_opclass_family(opclass);
+ /*
+ * If we have a range type, fall back on anyrange.
+ * This seems like a hack
+ * but I can't find any existing lookup function
+ * that knows about pseudotypes.
+ * compatible_oper is close but wants a *name*,
+ * and the point here is to avoid hardcoding a name
+ * (although it should always be = and &&).
+ *
+ * In addition for the normal key elements
+ * try both RTEqualStrategyNumber and BTEqualStrategyNumber.
+ * If you're using btree_gist then you'll need the latter.
+ */
+ if (isoverlaps)
+ {
+ *strat = RTOverlapStrategyNumber;
+ opname = "overlaps";
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+ }
+ else
+ {
+ *strat = RTEqualStrategyNumber;
+ opname = "equality";
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+
+ if (!OidIsValid(*opid))
+ {
+ *strat = BTEqualStrategyNumber;
+ *opid = get_opfamily_member(opfamily, atttype, atttype, *strat);
+ if (!OidIsValid(*opid) && type_is_range(atttype))
+ *opid = get_opfamily_member(opfamily, ANYRANGEOID, ANYRANGEOID, *strat);
+ }
+ }
+
+ if (!OidIsValid(*opid))
+ {
+ HeapTuple opftuple;
+ Form_pg_opfamily opfform;
+
+ /*
+ * attribute->opclass might not explicitly name the opfamily,
+ * so fetch the name of the selected opfamily for use in the
+ * error message.
+ */
+ opftuple = SearchSysCache1(OPFAMILYOID,
+ ObjectIdGetDatum(opfamily));
+ if (!HeapTupleIsValid(opftuple))
+ elog(ERROR, "cache lookup failed for opfamily %u",
+ opfamily);
+ opfform = (Form_pg_opfamily) GETSTRUCT(opftuple);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("no %s operator found for WITHOUT OVERLAPS constraint", opname),
+ errdetail("There must be an %s operator within opfamily \"%s\" for type \"%s\".",
+ opname,
+ NameStr(opfform->opfname),
+ format_type_be(atttype))));
+ }
+}
+
/*
* Compute per-index-column information, including indexed column numbers
* or index expressions, opclasses and their options. Note, all output vectors
@@ -1863,6 +1968,7 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
Oid accessMethodId,
bool amcanorder,
bool isconstraint,
+ bool istemporal,
Oid ddl_userid,
int ddl_sec_context,
int *ddl_save_nestlevel)
@@ -1886,6 +1992,14 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
else
nextExclOp = NULL;
+ /* exclusionOpNames can be non-NIL if we are creating a partition */
+ if (istemporal && 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);
@@ -2161,6 +2275,19 @@ ComputeIndexAttrs(IndexInfo *indexInfo,
indexInfo->ii_ExclusionStrats[attn] = strat;
nextExclOp = lnext(exclusionOpNames, nextExclOp);
}
+ else if (istemporal)
+ {
+ int strat;
+ Oid opid;
+ get_index_attr_temporal_operator(opclassOids[attn],
+ atttype,
+ attn == nkeycols - 1, /* TODO: Don't assume it's last? */
+ &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
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8a2c671b66..ba2a8914e4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -208,6 +208,7 @@ typedef struct NewConstraint
Oid refrelid; /* PK rel, if FOREIGN */
Oid refindid; /* OID of PK's index, if FOREIGN */
Oid conid; /* OID of pg_constraint entry, if FOREIGN */
+ bool contemporal; /* Whether the new constraint is temporal */
Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */
ExprState *qualstate; /* Execution state for CHECK expr */
} NewConstraint;
@@ -10003,6 +10004,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel,
conislocal, /* islocal */
coninhcount, /* inhcount */
connoinherit, /* conNoInherit */
+ false, /* conTemporal */
false); /* is_internal */
ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -10301,6 +10303,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel,
false,
1,
false,
+ false, /* conTemporal */
false);
/*
@@ -10806,6 +10809,7 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel)
false, /* islocal */
1, /* inhcount */
false, /* conNoInherit */
+ false, /* conTemporal */
true);
/* Set up partition dependencies for the new constraint */
@@ -11480,6 +11484,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
newcon->refrelid = con->confrelid;
newcon->refindid = con->conindid;
newcon->conid = con->oid;
+ newcon->contemporal = con->contemporal;
newcon->qual = (Node *) fkconstraint;
/* Find or create work queue entry for this table */
@@ -11646,10 +11651,12 @@ transformColumnNameList(Oid relId, List *colList,
*
* Look up the names, attnums, and types of the primary key attributes
* for the pkrel. Also return the index OID and index opclasses of the
- * index supporting the primary key.
+ * index supporting the primary key. If this is a temporal primary key,
+ * also set the WITHOUT OVERLAPS attribute name, attnum, and atttypid.
*
* All parameters except pkrel are output parameters. Also, the function
- * return value is the number of attributes in the primary key.
+ * return value is the number of attributes in the primary key,
+ * not including the WITHOUT OVERLAPS if any.
*
* Used when the column list in the REFERENCES specification is omitted.
*/
@@ -14191,7 +14198,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt)
if (CheckIndexCompatible(oldId,
stmt->accessMethod,
stmt->indexParams,
- stmt->excludeOpNames))
+ stmt->excludeOpNames,
+ stmt->istemporal))
{
Relation irel = index_open(oldId, NoLock);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 52177759ab..0577b60415 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, /* contemporal */
isInternal); /* is_internal */
}
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 5e97606793..6d0e946684 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, /* contemporal */
false); /* is_internal */
if (constrAddr)
ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d2032885e..3ea4acb74d 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -523,7 +523,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <node> TableElement TypedTableElement ConstraintElem TableFuncElement
%type <node> columnDef columnOptions
%type <defelt> def_elem reloption_elem old_aggr_elem operator_def_elem
-%type <node> def_arg columnElem where_clause where_or_current_clause
+%type <node> def_arg columnElem without_overlaps_clause
+ where_clause where_or_current_clause
a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
columnref in_expr having_clause func_table xmltable array_expr
OptWhereClause operator_def_arg
@@ -4096,7 +4097,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 without_overlaps_clause ')' opt_c_include opt_definition OptConsTableSpace
ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
@@ -4105,11 +4106,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;
@@ -4130,7 +4132,7 @@ ConstraintElem:
NULL, yyscanner);
$$ = (Node *) n;
}
- | PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace
+ | PRIMARY KEY '(' columnList without_overlaps_clause ')' opt_c_include opt_definition OptConsTableSpace
ConstraintAttributeSpec
{
Constraint *n = makeNode(Constraint);
@@ -4138,11 +4140,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;
@@ -4220,6 +4223,11 @@ columnList:
| columnList ',' columnElem { $$ = lappend($1, $3); }
;
+without_overlaps_clause:
+ ',' columnElem WITHOUT OVERLAPS { $$ = $2; }
+ | /*EMPTY*/ { $$ = NULL; }
+ ;
+
columnElem: ColId
{
$$ = (Node *) makeString($1);
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 55c315f0e2..e195410c23 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -124,6 +124,8 @@ static List *get_opclass(Oid opclass, Oid actual_datatype);
static void transformIndexConstraints(CreateStmtContext *cxt);
static IndexStmt *transformIndexConstraint(Constraint *constraint,
CreateStmtContext *cxt);
+static bool findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname,
+ Oid *typid);
static void transformExtendedStatistics(CreateStmtContext *cxt);
static void transformFKConstraints(CreateStmtContext *cxt,
bool skipValidation,
@@ -1725,6 +1727,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
index->unique = idxrec->indisunique;
index->nulls_not_distinct = idxrec->indnullsnotdistinct;
index->primary = idxrec->indisprimary;
+ index->istemporal = (idxrec->indisprimary || idxrec->indisunique) && idxrec->indisexclusion;
index->transformed = true; /* don't need transformIndexStmt */
index->concurrent = false;
index->if_not_exists = false;
@@ -1774,7 +1777,9 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx,
int nElems;
int i;
- Assert(conrec->contype == CONSTRAINT_EXCLUSION);
+ Assert(conrec->contype == CONSTRAINT_EXCLUSION ||
+ (index->istemporal &&
+ (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);
@@ -2314,6 +2319,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
}
index->nulls_not_distinct = constraint->nulls_not_distinct;
index->isconstraint = true;
+ index->istemporal = constraint->without_overlaps != NULL;
index->deferrable = constraint->deferrable;
index->initdeferred = constraint->initdeferred;
@@ -2406,6 +2412,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),
@@ -2682,6 +2693,65 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
notnullcmds = lappend(notnullcmds, notnullcmd);
}
}
+
+ /*
+ * Anything in without_overlaps should be included,
+ * but with the overlaps operator (&&) instead of equality.
+ */
+ if (constraint->without_overlaps != NULL) {
+ char *without_overlaps_str = strVal(constraint->without_overlaps);
+ IndexElem *iparam = makeNode(IndexElem);
+ char *typname;
+ Oid typid;
+
+ /*
+ * Iterate through the table's columns
+ * (like just a little bit above).
+ * If we find one whose name is the same as without_overlaps,
+ * validate that it's a range type.
+ *
+ * Otherwise report an error.
+ */
+
+ if (findNewOrOldColumn(cxt, without_overlaps_str, &typname, &typid))
+ {
+ if (!type_is_range(typid))
+ ereport(ERROR,
+ (errcode(ERRCODE_DATATYPE_MISMATCH),
+ errmsg("column \"%s\" in WITHOUT OVERLAPS is not a range type",
+ without_overlaps_str)));
+ }
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("range or PERIOD \"%s\" in WITHOUT OVERLAPS does not exist",
+ without_overlaps_str)));
+
+ iparam->name = pstrdup(without_overlaps_str);
+ iparam->expr = NULL;
+ iparam->indexcolname = NULL;
+ iparam->collation = NIL;
+ iparam->opclass = NIL;
+ iparam->opclassopts = NIL;
+ iparam->ordering = SORTBY_DEFAULT;
+ iparam->nulls_ordering = SORTBY_NULLS_DEFAULT;
+ index->indexParams = lappend(index->indexParams, iparam);
+
+ index->accessMethod = "gist";
+ constraint->access_method = "gist";
+
+ if (constraint->contype == CONSTR_PRIMARY)
+ {
+ /*
+ * Force the column to NOT NULL since it is part of the primary key.
+ */
+ AlterTableCmd *notnullcmd = makeNode(AlterTableCmd);
+
+ notnullcmd->subtype = AT_SetAttNotNull;
+ notnullcmd->name = pstrdup(without_overlaps_str);
+ notnullcmds = lappend(notnullcmds, notnullcmd);
+ }
+ }
}
/*
@@ -2801,6 +2871,58 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt)
return index;
}
+/*
+ * Tries to find a column by name among the existing ones (if it's an ALTER TABLE)
+ * and the new ones. Sets typname and typid if one is found. Returns false if we
+ * couldn't find a match.
+ */
+static bool
+findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname, Oid *typid)
+{
+ /* Check the new columns first in case their type is changing. */
+
+ ColumnDef *column = NULL;
+ ListCell *columns;
+
+ foreach(columns, cxt->columns)
+ {
+ column = lfirst_node(ColumnDef, columns);
+ if (strcmp(column->colname, colname) == 0)
+ {
+ *typid = typenameTypeId(NULL, column->typeName);
+ *typname = TypeNameToString(column->typeName);
+ return true;
+ }
+ }
+
+ /* Look up columns on existing table. */
+
+ if (cxt->isalter)
+ {
+ Relation rel = cxt->rel;
+ for (int i = 0; i < rel->rd_att->natts; i++)
+ {
+ Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
+ const char *attname;
+
+ if (attr->attisdropped)
+ continue;
+
+ attname = NameStr(attr->attname);
+ if (strcmp(attname, colname) == 0)
+ {
+ Type type = typeidType(attr->atttypid);
+ *typid = attr->atttypid;
+ *typname = pstrdup(typeTypeName(type));
+ ReleaseSysCache(type);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
/*
* transformExtendedStatistics
* Handle extended statistic objects
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 68f301484e..7aa5b62ab8 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -338,8 +338,8 @@ static char *deparse_expression_pretty(Node *expr, List *dpcontext,
static char *pg_get_viewdef_worker(Oid viewoid,
int prettyFlags, int wrapColumn);
static char *pg_get_triggerdef_worker(Oid trigid, bool pretty);
-static int decompile_column_index_array(Datum column_index_array, Oid relId,
- StringInfo buf);
+static int decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+ bool withoutOverlaps, StringInfo buf);
static char *pg_get_ruledef_worker(Oid ruleoid, int prettyFlags);
static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
const Oid *excludeOps,
@@ -2247,7 +2247,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_conkey);
- decompile_column_index_array(val, conForm->conrelid, &buf);
+ decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, &buf);
/* add foreign relation name */
appendStringInfo(&buf, ") REFERENCES %s(",
@@ -2258,7 +2258,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_confkey);
- decompile_column_index_array(val, conForm->confrelid, &buf);
+ decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, &buf);
appendStringInfoChar(&buf, ')');
@@ -2344,7 +2344,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
if (!isnull)
{
appendStringInfoString(&buf, " (");
- decompile_column_index_array(val, conForm->conrelid, &buf);
+ decompile_column_index_array(val, conForm->conrelid, InvalidOid, false, &buf);
appendStringInfoChar(&buf, ')');
}
@@ -2357,6 +2357,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
Oid indexId;
int keyatts;
HeapTuple indtup;
+ bool isnull;
/* Start off the constraint definition */
if (conForm->contype == CONSTRAINT_PRIMARY)
@@ -2379,7 +2380,14 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
val = SysCacheGetAttrNotNull(CONSTROID, tup,
Anum_pg_constraint_conkey);
- keyatts = decompile_column_index_array(val, conForm->conrelid, &buf);
+ /*
+ * If it has exclusion-style operator OIDs
+ * then it uses WITHOUT OVERLAPS.
+ */
+ indexId = conForm->conindid;
+ SysCacheGetAttr(CONSTROID, tup,
+ Anum_pg_constraint_conexclop, &isnull);
+ keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, &buf);
appendStringInfoChar(&buf, ')');
@@ -2574,8 +2582,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
* of keys.
*/
static int
-decompile_column_index_array(Datum column_index_array, Oid relId,
- StringInfo buf)
+decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId,
+ bool withoutOverlaps, StringInfo buf)
{
Datum *keys;
int nKeys;
@@ -2588,11 +2596,14 @@ decompile_column_index_array(Datum column_index_array, Oid relId,
for (j = 0; j < nKeys; j++)
{
char *colName;
+ int colid = DatumGetInt16(keys[j]);
- colName = get_attname(relId, DatumGetInt16(keys[j]), false);
+ colName = get_attname(relId, colid, false);
if (j == 0)
appendStringInfoString(buf, quote_identifier(colName));
+ else if (withoutOverlaps && j == nKeys - 1)
+ appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName));
else
appendStringInfo(buf, ", %s", quote_identifier(colName));
}
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 7234cb3da6..77a5b25889 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5536,8 +5536,9 @@ 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'
+ * associated exclusion constraint or temporal primary key/unique
+ * 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.
*/
@@ -5603,7 +5604,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->contemporal && (
+ 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 f7b6176692..efe8d72b32 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -6972,7 +6972,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
i_tablespace,
i_indreloptions,
i_indstatcols,
- i_indstatvals;
+ i_indstatvals,
+ i_withoutoverlaps;
/*
* We want to perform just one query against pg_index. However, we
@@ -7047,10 +7048,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.conexclop IS NOT NULL AS withoutoverlaps ");
+ else
+ appendPQExpBufferStr(query,
+ "null AS withoutoverlaps ");
/*
* The point of the messy-looking outer join is to find a constraint that
@@ -7125,6 +7133,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
i_indreloptions = PQfnumber(res, "indreloptions");
i_indstatcols = PQfnumber(res, "indstatcols");
i_indstatvals = PQfnumber(res, "indstatvals");
+ i_withoutoverlaps = PQfnumber(res, "withoutoverlaps");
indxinfo = (IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo));
@@ -7227,6 +7236,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
constrinfo->condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't';
constrinfo->conislocal = true;
constrinfo->separate = true;
+ constrinfo->withoutoverlaps = *(PQgetvalue(res, j, i_withoutoverlaps)) == 't';
indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
}
@@ -16922,9 +16932,22 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
break;
attname = getAttrName(indkey, tbinfo);
- appendPQExpBuffer(q, "%s%s",
- (k == 0) ? "" : ", ",
- fmtId(attname));
+ if (k == 0)
+ {
+ appendPQExpBuffer(q, "%s",
+ fmtId(attname));
+ }
+ else if (k == indxinfo->indnkeyattrs - 1 &&
+ coninfo->withoutoverlaps)
+ {
+ appendPQExpBuffer(q, ", %s WITHOUT OVERLAPS",
+ fmtId(attname));
+ }
+ else
+ {
+ appendPQExpBuffer(q, ", %s",
+ fmtId(attname));
+ }
}
if (indxinfo->indnkeyattrs < indxinfo->indnattrs)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 9036b13f6a..773a2de18b 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -489,6 +489,7 @@ typedef struct _constraintInfo
bool condeferred; /* true if constraint is INITIALLY DEFERRED */
bool conislocal; /* true if constraint has local definition */
bool separate; /* true if must dump as separate item */
+ bool withoutoverlaps; /* true if the last elem is WITHOUT OVERLAPS */
} ConstraintInfo;
typedef struct _procLangInfo
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 0758fe5ea0..22aa87d3dc 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -1015,6 +1015,52 @@ my %tests = (
},
},
+ 'ALTER TABLE ONLY test_table_tpk ADD 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,
+ exclude_test_table => 1,
+ },
+ unlike => {
+ only_dump_test_table => 1,
+ exclude_dump_test_schema => 1,
+ only_dump_measurement => 1,
+ },
+ },
+
+ 'ALTER TABLE ONLY test_table_tuq ADD 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,
+ exclude_test_table => 1,
+ },
+ unlike => {
+ only_dump_test_table => 1,
+ 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 bac94a338c..4fbfc07460 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -2384,6 +2384,10 @@ describeOneTableDetails(const char *schemaname,
else
appendPQExpBufferStr(&buf, ", false AS indisreplident");
appendPQExpBufferStr(&buf, ", c2.reltablespace");
+ if (pset.sversion >= 160000)
+ appendPQExpBufferStr(&buf, ", con.contemporal");
+ else
+ appendPQExpBufferStr(&buf, ", false AS contemporal");
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"
@@ -2405,8 +2409,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 temporal PK/UNIQUE constraint,
+ * 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/catalog/index.h b/src/include/catalog/index.h
index 4d8ba81f90..a604546f48 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -91,6 +91,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_TEMPORAL (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 9b9d8faf35..786a9ca904 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 and foreign keys, signifies the last column is a range
+ * and should use overlaps instead of equals.
+ */
+ bool contemporal;
+
#ifdef CATALOG_VARLEN /* variable-length fields start here */
/*
@@ -146,7 +152,7 @@ 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 temporal primary keys.
*/
Oid conexclop[1] BKI_LOOKUP(pg_operator);
@@ -236,6 +242,7 @@ extern Oid CreateConstraintEntry(const char *constraintName,
bool conIsLocal,
int conInhCount,
bool conNoInherit,
+ bool conTemporal,
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 179eb9901f..f10886f6ca 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -44,7 +44,8 @@ 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 istemporal);
extern Oid GetDefaultOpClass(Oid type_id, Oid am_id);
extern Oid ResolveOpClass(const List *opclass, Oid attrType,
const char *accessMethodName, Oid accessMethodId);
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index fef4c714b8..0747b54b26 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2641,6 +2641,9 @@ typedef struct Constraint
Oid old_pktable_oid; /* pg_constraint.confrelid of my former
* self */
+ /* Fields used for temporal PRIMARY KEY and FOREIGN KEY constraints: */
+ Node *without_overlaps; /* String node naming range column */
+
/* Fields used for constraints that allow a NOT VALID specification */
bool skip_validation; /* skip validation of existing rows? */
bool initially_valid; /* mark the new constraint as valid? */
@@ -3242,6 +3245,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 istemporal; /* is it for a temporal pkey? */
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..5e126df071
--- /dev/null
+++ b/src/test/regress/expected/without_overlaps.out
@@ -0,0 +1,359 @@
+-- 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: syntax error at or near "WITHOUT"
+LINE 3: CONSTRAINT temporal_rng_pk PRIMARY KEY (valid_at WITHOUT OV...
+ ^
+-- 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: range or PERIOD "valid_at" in WITHOUT OVERLAPS does not exist
+-- PK with a non-range column:
+CREATE TABLE temporal_rng (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: column "valid_at" in WITHOUT OVERLAPS is not a range 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:
+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)
+
+DROP TABLE temporal_rng2;
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng2 (
+ id int4range,
+ valid_at textrange2,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng2 DROP CONSTRAINT temporal_rng2_pk;
+DROP TABLE temporal_rng2;
+DROP TYPE textrange2;
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+CREATE TABLE temporal_rng2 (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+ERROR: syntax error at or near "WITHOUT"
+LINE 3: CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLA...
+ ^
+-- UNIQUE with a range column/PERIOD that isn't there:
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: range or PERIOD "valid_at" in WITHOUT OVERLAPS does not exist
+-- UNIQUE with a non-range column:
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+ERROR: column "valid_at" in WITHOUT OVERLAPS is not a range type
+-- UNIQUE with one column plus a range:
+CREATE TABLE temporal_rng2 (
+ -- 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_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+ Table "public.temporal_rng2"
+ Column | Type | Collation | Nullable | Default
+----------+-----------+-----------+----------+---------
+ id | int4range | | |
+ valid_at | tsrange | | |
+Indexes:
+ "temporal_rng2_uq" UNIQUE (id, valid_at WITHOUT OVERLAPS)
+
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_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_rng2_uq';
+ pg_get_indexdef
+---------------------------------------------------------------------------------
+ CREATE UNIQUE INDEX temporal_rng2_uq ON temporal_rng2 USING gist (id, valid_at)
+(1 row)
+
+-- 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;
+--
+-- 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;
+-- 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 4df9d8503b..66b75fa12f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
# geometry depends on point, lseg, line, box, path, polygon, circle
# horology depends on date, time, timetz, timestamp, timestamptz, interval
# ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc without_overlaps
# ----------
# Load huge amounts of data
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
new file mode 100644
index 0000000000..3c3236618c
--- /dev/null
+++ b/src/test/regress/sql/without_overlaps.sql
@@ -0,0 +1,262 @@
+-- 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 INTEGER,
+ 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:
+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';
+DROP TABLE temporal_rng2;
+
+
+-- PK with a custom range type:
+CREATE TYPE textrange2 AS range (subtype=text, collation="C");
+CREATE TABLE temporal_rng2 (
+ id int4range,
+ valid_at textrange2,
+ CONSTRAINT temporal_rng2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
+);
+ALTER TABLE temporal_rng2 DROP CONSTRAINT temporal_rng2_pk;
+DROP TABLE temporal_rng2;
+DROP TYPE textrange2;
+
+-- UNIQUE with no columns just WITHOUT OVERLAPS:
+
+CREATE TABLE temporal_rng2 (
+ valid_at tsrange,
+ CONSTRAINT temporal_rng2_uq UNIQUE (valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a range column/PERIOD that isn't there:
+
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with a non-range column:
+
+CREATE TABLE temporal_rng2 (
+ id INTEGER,
+ valid_at TEXT,
+ CONSTRAINT temporal_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+
+-- UNIQUE with one column plus a range:
+
+CREATE TABLE temporal_rng2 (
+ -- 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_rng2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS)
+);
+\d temporal_rng2
+SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+SELECT pg_get_indexdef(conindid, 0, true) FROM pg_constraint WHERE conname = 'temporal_rng2_uq';
+
+-- 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;
+
+--
+-- 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;
+
+-- 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;
--
2.25.1