diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index a8a735c247..c12fb6e42c 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2920,6 +2920,7 @@ _copyConstraint(const Constraint *from) COPY_SCALAR_FIELD(fk_del_action); COPY_NODE_FIELD(old_conpfeqop); COPY_SCALAR_FIELD(old_pktable_oid); + COPY_NODE_FIELD(without_overlaps); COPY_SCALAR_FIELD(skip_validation); COPY_SCALAR_FIELD(initially_valid); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index e23e68fdb3..163a976d04 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -470,7 +470,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type TableElement TypedTableElement ConstraintElem TableFuncElement %type columnDef columnOptions %type def_elem reloption_elem old_aggr_elem operator_def_elem -%type def_arg columnElem where_clause where_or_current_clause +%type def_arg columnElem withoutOverlapsClause 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 ExclusionWhereClause operator_def_arg @@ -3459,6 +3459,7 @@ ColConstraintElem: n->contype = CONSTR_PRIMARY; n->location = @1; n->keys = NULL; + n->without_overlaps = NULL; n->options = $3; n->indexname = NULL; n->indexspace = $4; @@ -3651,18 +3652,19 @@ ConstraintElem: NULL, yyscanner); $$ = (Node *)n; } - | PRIMARY KEY '(' columnList ')' opt_c_include opt_definition OptConsTableSpace + | PRIMARY KEY '(' columnList withoutOverlapsClause ')' opt_c_include opt_definition OptConsTableSpace ConstraintAttributeSpec { Constraint *n = makeNode(Constraint); 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; @@ -3673,6 +3675,7 @@ ConstraintElem: n->contype = CONSTR_PRIMARY; n->location = @1; n->keys = NIL; + n->without_overlaps = NULL; n->including = NIL; n->options = NIL; n->indexname = $3; @@ -3736,6 +3739,11 @@ columnList: | columnList ',' columnElem { $$ = lappend($1, $3); } ; +withoutOverlapsClause: + ',' 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 a37d1f18be..d7a2646491 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -1872,7 +1872,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) index = makeNode(IndexStmt); - index->unique = (constraint->contype != CONSTR_EXCLUSION); + index->unique = (constraint->contype != CONSTR_EXCLUSION && constraint->without_overlaps == NULL); index->primary = (constraint->contype == CONSTR_PRIMARY); if (index->primary) { @@ -2239,6 +2239,142 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) iparam->ordering = SORTBY_DEFAULT; iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; index->indexParams = lappend(index->indexParams, iparam); + + if (constraint->without_overlaps != NULL) + { + /* + * We are building the index like for an EXCLUSION constraint, + * so use the equality operator for these elements. + */ + List *opname = list_make1(makeString("=")); + index->excludeOpNames = lappend(index->excludeOpNames, opname); + } + } + + /* + * Anything in without_overlaps should be included, + * but with the overlaps operator (&&) instead of equality. + */ + if (constraint->without_overlaps != NULL) { + // char *without_overlaps_str = nodeToString(constraint->without_overlaps); + char *without_overlaps_str = strVal(constraint->without_overlaps); + IndexElem *iparam = makeNode(IndexElem); + + /* + * 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 iterate through the table's non-system PERIODs, + * and if we find one then use its start/end columns + * to construct a range expression. + * + * Otherwise report an error. + */ + bool found = false; + ColumnDef *column = NULL; + ListCell *columns; + if (cxt->isalter) + { + // TODO: DRY this up with the non-ALTER case: + Relation rel = cxt->rel; + /* + * Look up columns on existing table. + */ + for (int i = 0; i < rel->rd_att->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i); + const char *attname = NameStr(attr->attname); + if (strcmp(attname, without_overlaps_str) == 0) + { + if (type_is_range(attr->atttypid)) + { + found = true; + break; + } + else + { + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" named in WITHOUT OVERLAPS is not a range type", + without_overlaps_str))); + } + } + } + } + else + { + /* + * Look up columns on the being-created table. + */ + foreach(columns, cxt->columns) + { + column = castNode(ColumnDef, lfirst(columns)); + // ereport(NOTICE, (errmsg("range %s vs column %s of type %d", without_overlaps_str, column->colname, column->typeName->typeOid))); + if (strcmp(column->colname, without_overlaps_str) == 0) + { + Oid colTypeOid = typenameTypeId(NULL, column->typeName); + if (type_is_range(colTypeOid)) + { + found = true; + break; + } + else + { + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" named in WITHOUT OVERLAPS is not a range type", + without_overlaps_str))); + } + } + } + } + if (found) + { + iparam->name = without_overlaps_str; // TODO: pstrdup here? + iparam->expr = NULL; + } + else { + found = false; + /* + * TODO: Search for a non-system PERIOD with the right name. + */ + if (found) + { + iparam->name = NULL; + /* + * TODO: Build up a parse tree to cast the period to a range. + * See transformExpr (called below and defined in parser/parse_expr.c. + */ + /* + TypeCast *expr = makeNode(TypeCast); + expr->arg = constraint->without_overlaps; + expr->typeName = "...."; // TODO: need to look up which range type to use + expr->location = -1; + iparam->expr = transformExpr(..., expr, EXPR_KIND_INDEX_EXPRESSION); + */ + } + else + { + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("range or PERIOD \"%s\" named in WITHOUT OVERLAPS does not exist", + without_overlaps_str))); + } + } + + iparam->indexcolname = NULL; + iparam->collation = NIL; + iparam->opclass = NIL; + iparam->ordering = SORTBY_DEFAULT; + iparam->nulls_ordering = SORTBY_NULLS_DEFAULT; + index->indexParams = lappend(index->indexParams, iparam); + + List *opname = list_make1(makeString("&&")); + index->excludeOpNames = lappend(index->excludeOpNames, opname); + index->accessMethod = "gist"; + constraint->access_method = "gist"; } } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 85055bbb95..a9d18ed94e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -317,7 +317,7 @@ 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); + 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, @@ -1961,7 +1961,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, elog(ERROR, "null conkey for constraint %u", constraintId); - decompile_column_index_array(val, conForm->conrelid, &buf); + decompile_column_index_array(val, conForm->conrelid, false, &buf); /* add foreign relation name */ appendStringInfo(&buf, ") REFERENCES %s(", @@ -1975,7 +1975,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, elog(ERROR, "null confkey for constraint %u", constraintId); - decompile_column_index_array(val, conForm->confrelid, &buf); + decompile_column_index_array(val, conForm->confrelid, false, &buf); appendStringInfoChar(&buf, ')'); @@ -2076,7 +2076,13 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, elog(ERROR, "null conkey for constraint %u", constraintId); - keyatts = decompile_column_index_array(val, conForm->conrelid, &buf); + /* + * If it has exclusion-style operator OIDs + * then it uses WITHOUT OVERLAPS. + */ + SysCacheGetAttr(CONSTROID, tup, + Anum_pg_constraint_conexclop, &isnull); + keyatts = decompile_column_index_array(val, conForm->conrelid, !isnull, &buf); appendStringInfoChar(&buf, ')'); @@ -2272,7 +2278,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, */ static int decompile_column_index_array(Datum column_index_array, Oid relId, - StringInfo buf) + bool withoutOverlaps, StringInfo buf) { Datum *keys; int nKeys; @@ -2290,9 +2296,17 @@ decompile_column_index_array(Datum column_index_array, Oid relId, colName = get_attname(relId, DatumGetInt16(keys[j]), 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)); + } } return nKeys; diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index d9ffb78484..3dbcf5b507 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4949,8 +4949,9 @@ restart: * 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. + * 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. */ @@ -5016,7 +5017,12 @@ RelationGetExclusionInfo(Relation indexRelation, int nelem; /* We want the exclusion constraint owning the index */ - if (conform->contype != CONSTRAINT_EXCLUSION || + /* + * TODO: Is this too permissive? + * Maybe it needs to be (!= CONSTRAINT_PRIMARY || !has_excl_operators) + */ + if ((conform->contype != CONSTRAINT_EXCLUSION && + conform->contype != CONSTRAINT_PRIMARY) || conform->conindid != RelationGetRelid(indexRelation)) continue; diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index e962ae7e91..7427711894 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -6825,7 +6825,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) i_tablespace, i_indreloptions, i_indstatcols, - i_indstatvals; + i_indstatvals, + i_withoutoverlaps; int ntups; for (i = 0; i < numTables; i++) @@ -6887,7 +6888,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) "(SELECT pg_catalog.array_agg(attstattarget ORDER BY attnum) " " FROM pg_catalog.pg_attribute " " WHERE attrelid = i.indexrelid AND " - " attstattarget >= 0) AS indstatvals " + " attstattarget >= 0) AS indstatvals, " + "c.conexclop IS NOT NULL AS withoutoverlaps " "FROM pg_catalog.pg_index i " "JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) " "JOIN pg_catalog.pg_class t2 ON (t2.oid = i.indrelid) " @@ -6926,7 +6928,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) "(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, " "t.reloptions AS indreloptions, " "'' AS indstatcols, " - "'' AS indstatvals " + "'' AS indstatvals, " + "null AS withoutoverlaps " "FROM pg_catalog.pg_index i " "JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) " "LEFT JOIN pg_catalog.pg_constraint c " @@ -6961,7 +6964,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) "(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, " "t.reloptions AS indreloptions, " "'' AS indstatcols, " - "'' AS indstatvals " + "'' AS indstatvals, " + "null AS withoutoverlaps " "FROM pg_catalog.pg_index i " "JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) " "LEFT JOIN pg_catalog.pg_constraint c " @@ -6992,7 +6996,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) "(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, " "t.reloptions AS indreloptions, " "'' AS indstatcols, " - "'' AS indstatvals " + "'' AS indstatvals, " + "null AS withoutoverlaps " "FROM pg_catalog.pg_index i " "JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) " "LEFT JOIN pg_catalog.pg_depend d " @@ -7026,7 +7031,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) "(SELECT spcname FROM pg_catalog.pg_tablespace s WHERE s.oid = t.reltablespace) AS tablespace, " "null AS indreloptions, " "'' AS indstatcols, " - "'' AS indstatvals " + "'' AS indstatvals, " + "null AS withoutoverlaps " "FROM pg_catalog.pg_index i " "JOIN pg_catalog.pg_class t ON (t.oid = i.indexrelid) " "LEFT JOIN pg_catalog.pg_depend d " @@ -7066,6 +7072,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"); tbinfo->indexes = indxinfo = (IndxInfo *) pg_malloc(ntups * sizeof(IndxInfo)); @@ -7125,6 +7132,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) constrinfo[j].condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't'; constrinfo[j].conislocal = true; constrinfo[j].separate = true; + constrinfo[j].withoutoverlaps = *(PQgetvalue(res, j, i_withoutoverlaps)) == 't'; indxinfo[j].indexconstraint = constrinfo[j].dobj.dumpId; } @@ -16578,9 +16586,22 @@ dumpConstraint(Archive *fout, 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 2e1b90acd0..4816cc5f79 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -448,6 +448,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 de6895122e..b16d991caf 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -632,6 +632,28 @@ my %tests = ( }, }, + 'ALTER TABLE ONLY test_table 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, + }, + }, + '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 779e48437c..baf15bac06 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -2318,6 +2318,8 @@ describeOneTableDetails(const char *schemaname, } /* Everything after "USING" is echoed verbatim */ + // TODO: Show WITHOUT OVERLAPS info here? + // It is not really part of the *index*. indexdef = PQgetvalue(result, i, 5); usingpos = strstr(indexdef, " USING "); if (usingpos) diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index c87bdedbbb..efb764daba 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -136,7 +136,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]; diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index fe35783359..60d595ba44 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2154,6 +2154,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 PERIOD or 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? */ diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index aaaa488b3c..ecdc1b1dab 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -213,6 +213,7 @@ timestamptz_tbl|f timetz_tbl|f varchar_tbl|f view_base_table|t +without_overlaps_test|t -- restore normal output mode \a\t -- diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out new file mode 100644 index 0000000000..53475c1e7e --- /dev/null +++ b/src/test/regress/expected/without_overlaps.out @@ -0,0 +1,104 @@ +-- Tests for WITHOUT OVERLAPS. +-- +-- test input parser +-- +-- PK with no columns just WITHOUT OVERLAPS: +CREATE TABLE without_overlaps_test ( + valid_at tsrange, + CONSTRAINT without_overlaps_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS) +); +ERROR: syntax error at or near "WITHOUT" +LINE 3: ...STRAINT without_overlaps_pk PRIMARY KEY (valid_at WITHOUT OV... + ^ +-- PK with a range column that isn't there: +CREATE TABLE without_overlaps_test ( + id INTEGER, + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ERROR: range or PERIOD "valid_at" named in WITHOUT OVERLAPS does not exist +-- PK with a PERIOD that isn't there: +-- PK with a non-range column: +CREATE TABLE without_overlaps_test ( + id INTEGER, + valid_at TEXT, + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ERROR: column "valid_at" named in WITHOUT OVERLAPS is not a range type +-- PK with one column plus a range: +CREATE TABLE without_overlaps_test ( + -- 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 without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +-- PK with two columns plus a range: +CREATE TABLE without_overlaps_test2 ( + id1 int4range, + id2 int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_test2; +-- PK with one column plus a PERIOD: +-- PK with two columns plus a PERIOD: +-- PK with a custom range type: +CREATE TYPE textrange2 AS range (subtype=text, collation="C"); +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at textrange2, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE without_overlaps_test2 DROP CONSTRAINT without_overlaps2_pk; +DROP TABLE without_overlaps_test2; +DROP TYPE textrange2; +DROP TABLE without_overlaps_test; +CREATE TABLE without_overlaps_test ( + id int4range, + valid_at tsrange +); +ALTER TABLE without_overlaps_test + ADD CONSTRAINT without_overlaps_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'without_overlaps_pk'; + pg_get_constraintdef +--------------------------------------------- + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +(1 row) + +-- +-- test PK inserts +-- +-- okay: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03')); +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04')); +INSERT INTO without_overlaps_test VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05')); +INSERT INTO without_overlaps_test VALUES ('[3,3]', tsrange('2018-01-01', NULL)); +-- should fail: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05')); +ERROR: conflicting key value violates exclusion constraint "without_overlaps_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 without_overlaps_test VALUES (NULL, tsrange('2018-01-01', '2018-01-05')); +ERROR: null value in column "id" 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 without_overlaps_test VALUES ('[3,3]', NULL); +ERROR: null value in column "valid_at" violates not-null constraint +DETAIL: Failing row contains ([3,4), null). +-- +-- test changing the PK's dependencies +-- +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at DROP NOT NULL; +ERROR: column "valid_at" is in a primary key +ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at)); +ALTER TABLE without_overlaps_test2 RENAME COLUMN valid_at TO valid_thru; +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_thru; +DROP TABLE without_overlaps_test2; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 4051a4ad4e..7f5d975d16 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -122,7 +122,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr # ---------- # Another group of parallel tests # ---------- -test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info +test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info without_overlaps # event triggers cannot run concurrently with any test that runs DDL test: event_trigger diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index ac1ea622d6..5e2e68c3a3 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -19,6 +19,7 @@ test: uuid test: enum test: money test: rangetypes +test: without_overlaps test: pg_lsn test: regproc test: strings diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql new file mode 100644 index 0000000000..7084c68904 --- /dev/null +++ b/src/test/regress/sql/without_overlaps.sql @@ -0,0 +1,114 @@ +-- Tests for WITHOUT OVERLAPS. + +-- +-- test input parser +-- + +-- PK with no columns just WITHOUT OVERLAPS: + +CREATE TABLE without_overlaps_test ( + valid_at tsrange, + CONSTRAINT without_overlaps_pk PRIMARY KEY (valid_at WITHOUT OVERLAPS) +); + +-- PK with a range column that isn't there: + +CREATE TABLE without_overlaps_test ( + id INTEGER, + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); + +-- PK with a PERIOD that isn't there: +-- TODO + +-- PK with a non-range column: + +CREATE TABLE without_overlaps_test ( + id INTEGER, + valid_at TEXT, + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); + +-- PK with one column plus a range: + +CREATE TABLE without_overlaps_test ( + -- 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 without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); + +-- PK with two columns plus a range: +CREATE TABLE without_overlaps_test2 ( + id1 int4range, + id2 int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_test2; + + +-- PK with one column plus a PERIOD: +-- TODO + +-- PK with two columns plus a PERIOD: +-- TODO + +-- PK with a custom range type: +CREATE TYPE textrange2 AS range (subtype=text, collation="C"); +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at textrange2, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE without_overlaps_test2 DROP CONSTRAINT without_overlaps2_pk; +DROP TABLE without_overlaps_test2; +DROP TYPE textrange2; + +DROP TABLE without_overlaps_test; +CREATE TABLE without_overlaps_test ( + id int4range, + valid_at tsrange +); +ALTER TABLE without_overlaps_test + ADD CONSTRAINT without_overlaps_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'without_overlaps_pk'; + +-- +-- test PK inserts +-- + +-- okay: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-03')); +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-03-03', '2018-04-04')); +INSERT INTO without_overlaps_test VALUES ('[2,2]', tsrange('2018-01-01', '2018-01-05')); +INSERT INTO without_overlaps_test VALUES ('[3,3]', tsrange('2018-01-01', NULL)); + +-- should fail: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-01-01', '2018-01-05')); +INSERT INTO without_overlaps_test VALUES (NULL, tsrange('2018-01-01', '2018-01-05')); +INSERT INTO without_overlaps_test VALUES ('[3,3]', NULL); + +-- +-- test changing the PK's dependencies +-- + +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); + +ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at DROP NOT NULL; +ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at TYPE tstzrange USING tstzrange(lower(valid_at), upper(valid_at)); +ALTER TABLE without_overlaps_test2 RENAME COLUMN valid_at TO valid_thru; +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_thru; +DROP TABLE without_overlaps_test2;