Add temporal PRIMARY KEY and UNIQUE constraints
authorPeter Eisentraut <[email protected]>
Wed, 24 Jan 2024 14:43:41 +0000 (15:43 +0100)
committerPeter Eisentraut <[email protected]>
Wed, 24 Jan 2024 15:34:37 +0000 (16:34 +0100)
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 <[email protected]>
Reviewed-by: Peter Eisentraut <[email protected]>
Reviewed-by: jian he <[email protected]>
Discussion: https://www.postgresql.org/message-id/flat/CA+renyUApHgSZF9-nd-a0+OPGharLQLO=mDHcY4_qQ0+noCUVg@mail.gmail.com

34 files changed:
contrib/btree_gist/Makefile
contrib/btree_gist/expected/without_overlaps.out [new file with mode: 0644]
contrib/btree_gist/meson.build
contrib/btree_gist/sql/without_overlaps.sql [new file with mode: 0644]
doc/src/sgml/catalogs.sgml
doc/src/sgml/gist.sgml
doc/src/sgml/ref/create_table.sgml
src/backend/access/gist/gistutil.c
src/backend/catalog/heap.c
src/backend/catalog/index.c
src/backend/catalog/pg_constraint.c
src/backend/commands/indexcmds.c
src/backend/commands/tablecmds.c
src/backend/commands/trigger.c
src/backend/commands/typecmds.c
src/backend/nodes/outfuncs.c
src/backend/nodes/readfuncs.c
src/backend/parser/gram.y
src/backend/parser/parse_utilcmd.c
src/backend/utils/adt/ruleutils.c
src/backend/utils/cache/relcache.c
src/bin/pg_dump/pg_dump.c
src/bin/pg_dump/pg_dump.h
src/bin/pg_dump/t/002_pg_dump.pl
src/bin/psql/describe.c
src/include/access/gist.h
src/include/catalog/catversion.h
src/include/catalog/index.h
src/include/catalog/pg_constraint.h
src/include/commands/defrem.h
src/include/nodes/parsenodes.h
src/test/regress/expected/without_overlaps.out [new file with mode: 0644]
src/test/regress/parallel_schedule
src/test/regress/sql/without_overlaps.sql [new file with mode: 0644]

index 9ab8548bc0ed1df0e41aac3b4f70988621b63104..7ac2df26c1044a74a8152927795dd16d2016acd4 100644 (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))
 
diff --git a/contrib/btree_gist/expected/without_overlaps.out b/contrib/btree_gist/expected/without_overlaps.out
new file mode 100644 (file)
index 0000000..be52c52
--- /dev/null
@@ -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)).
index a44ce905e5943187868a6668f578a017e346886a..73b1bbf52a6c085023bb473e59006ea5c05729f0 100644 (file)
@@ -91,6 +91,7 @@ tests += {
       'bool',
       'partitions',
       'stratnum',
+      'without_overlaps',
     ],
   },
 }
diff --git a/contrib/btree_gist/sql/without_overlaps.sql b/contrib/btree_gist/sql/without_overlaps.sql
new file mode 100644 (file)
index 0000000..433c609
--- /dev/null
@@ -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)');
index c15d861e823deec8c176de4e3d2862c765324290..16b94461b2ce0ab43636a7ff03a5ac0ca836ce54 100644 (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>
index 7c20f8467b484516c7cce5c753c3953d1819bc63..8a19f156d83c3a19e54733cfca97b912803366a5 100644 (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:
index e04a0692c4e9970906c04a04b34ca76747e164f0..079af9126ad0f8be81baa1e71bcbec2b7d1b2a4a 100644 (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>
index d4d08bd118ff86aa869f5a51fe516ba4e9fe9fa4..00570445133cd5999a368e001151fec7426402d8 100644 (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);
+}
index 45a71081d429220f5e8be693c258417eb01cb746..c73f7bcd0113fe675b0409329732883b8144fe55 100644 (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;
 }
index 80ba25c76fbb76dae52d9cdd0296ef9e04209c9c..86784efd384eedc3316d6433dc7adaee7dab1371 100644 (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);
 
    /*
index a0232aa1bbf4ef96799ffc8a3d06eb33b4cb416c..0a95608179d1de5ffe3728fb8a2f1ee21c270d8a 100644 (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);
index 340248a3f29400f9e06027c5da0054f53a261277..7a87626f5f01822e19aecfb5819bc05189a072d2 100644 (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()
  *
index 5ccd0eec1c7223dad3216cb4721acfd053622e01..eceacd4ebc3f505039ae6f42b7de48cf83d6a416 100644 (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);
 
index 0880ca51fb27f017e1952c55ca899ee03af6bbe4..c344ff09442b5b3ddd71a9b30536c56c7bfcf812 100644 (file)
@@ -841,6 +841,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
                                              true, /* islocal */
                                              0,    /* inhcount */
                                              true, /* noinherit */
+                                             false,    /* conwithoutoverlaps */
                                              isInternal);  /* is_internal */
    }
 
index 27cc186c16f25c8728b0d3bb8648345e9aa07b86..a400fb39f67bf91f9c62bcdc80126d9e0cab1e1a 100644 (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);
index 296ba8451879c0c2f1a01f96922ebfb4f8f5e44f..03f67b6850623dab2647d5ea6c56150c11640625 100644 (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);
index 1624b345812cf300e77c3613d1ac3d3d5212cd68..cfb552fde7499c758ce369d476f1ff474693f38c 100644 (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);
index 3460fea56bacf8ef6b26d786b9c3965cc2a5aa6c..130f7fc7c3f0a04116e5a5af5c24216f5b968872 100644 (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; }
index b2d1fa9d0d6467cfe5f310ddf70e6291aa7c0d9e..56ac4f516ea09c0df5d8b668b3a54049cf6cf320 100644 (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";
+       }
+
    }
 
    /*
index 0b2a164057bfc6c81801720d83c6c7754eaadb4d..b625f471a8475c68c077f8040c45def11da999ca 100644 (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, ')');
 
index 20273f0be16fb85d96a838b066ee12e8945c0722..5ef8f2f0caa7b173078f894dcab282114524ab3d 100644 (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;
 
index bc20a025ce43dafb0db41090447852c05e5045a0..a19443becd69061c2d7f39fb0c3226bdc3f76a35 100644 (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 (");
index f0772d21579525c5a0af8e7dad5bb129fc596c7b..93d97a40900152982a4b31dce78f073c0080868e 100644 (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;
index 3912dbf481ae428af84beb9081e480b28439a31a..00b5092713d06599f6b904839fe17ade9a7628e6 100644 (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 (
index 37f95163201be6477fbdf63c11841004b2d9e560..9cd8783325c435cb240904819b32ff796c4e5842 100644 (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));
index e7ced18a5ba5fadd96174ea08fd094d36a99efc7..22dd04c1418524d0c496422a7539f73900223070 100644 (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 */
index 6cbe33b6fa876699b5d0e548098bac92cdbc87d5..43d83672a6fa86b2b06b1efee1f634559b9ab0d3 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 202401201
+#define CATALOG_VERSION_NO 202401241
 
 #endif
index 99dab5940bc5e9a5c864b851adc74ebbbe6bdafd..2ef8512dbffe8fc28e04995f7b5af5750d85718f 100644 (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,
index c693091d426469f9b831b2fef64f80d1cdac9fa6..01e6bc21cd1fb8168487f4c4b4b8a4b8ddd170fa 100644 (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,
index 29c511e319693bd748a8aef59f372b9c3c760d8b..0c53d67d3eef8780b8a1423ec3758aa41d8f7055 100644 (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);
index b3181f34aee85f644b4b518f1fa2dc45d43d2c2e..476d55dd2408eb7907e023d5475cc42b496d70dd 100644 (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 */
diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out
new file mode 100644 (file)
index 0000000..726e941
--- /dev/null
@@ -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;
index 6f5a33c234ecaf07dc86255baf586e863ee234c4..1d8a414eea7c1786542f3ee99495bbcdceb30737 100644 (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
diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql
new file mode 100644 (file)
index 0000000..c8e8ab9
--- /dev/null
@@ -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;