Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1mn8Is-0006PW-5b for pgsql-hackers@arkaria.postgresql.org; Tue, 16 Nov 2021 23:55:55 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1mn8Ip-0004ZO-O6 for pgsql-hackers@arkaria.postgresql.org; Tue, 16 Nov 2021 23:55:51 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1mn8Io-0004ZE-Rb for pgsql-hackers@lists.postgresql.org; Tue, 16 Nov 2021 23:55:51 +0000 Received: from mail-pj1-x1034.google.com ([2607:f8b0:4864:20::1034]) by magus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.92) (envelope-from ) id 1mn8IX-0000qh-Iu for pgsql-hackers@lists.postgresql.org; Tue, 16 Nov 2021 23:55:50 +0000 Received: by mail-pj1-x1034.google.com with SMTP id fv9-20020a17090b0e8900b001a6a5ab1392so892616pjb.1 for ; Tue, 16 Nov 2021 15:55:33 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=illuminatedcomputing-com.20210112.gappssmtp.com; s=20210112; h=subject:from:to:cc:references:message-id:date:user-agent :mime-version:in-reply-to:content-language; bh=KsZh5VzWmmv/jIvkcU9OOulqOZN1V7h9dOwB+zYULKU=; b=JyISd0KmDQfwHABG6/PvvxtWXaK7HomlJyYsTHisq+vLyqYm0G94a+kmDzAwYprPlh hmDlL8QJ6bkc6s40bz7tfR6zuusVgSeA6h/gx/7pPEO6+chWkTwZa87U1+IgQ8qpO/a+ Gq9cKdQFPbMO0NmpJqBhv/IBhcdvtm+RRyqy6+BlepZgPMw5Iivv7c0R9TjfrixLcRtw jSh9lJMivTy36xSx2b4H0vBtYgsqh6hECgZLZvQG2OnOptalMXqbqsltfRKTHLPuL194 ugbqiOGdMaperg+DaKrpjE2rQy6jxqOz7vrM4SCOyn6aylSPEQEMzeBeP29Ung/cqNQf mF3g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:subject:from:to:cc:references:message-id:date :user-agent:mime-version:in-reply-to:content-language; bh=KsZh5VzWmmv/jIvkcU9OOulqOZN1V7h9dOwB+zYULKU=; b=ZsqNtX7R+iGcfVF3p/cOD0NWQiXdRa7ZKOropNb/oQJP5GlYqPC8b4Rv1Gp+SMeZFO wPhMtbtdYXholHqsLmXHwh/T5XGFQnmnsUxCQN1dhh0w4yW3Y0EWYBZOoopKt87QuoSh 2NCxBKwa6fKD+ZcF06KAUjDMTlXMgqkKvjwy3UVU602AHgANppzR4ihrcMWOtknMSF/t +yRGvdz0IjLmSuOI+xb4+7rVeDYiHHDKH0ygOpqoZguKyTHkVpvz8dfSeDrZXpXSXLtc 0RmALv6d12MfyIzZBvOymMxWtZh4P6yCAAzK6MgaK5AUH/0zPJqZEBzux1HBCr7iLmsm TmDQ== X-Gm-Message-State: AOAM531yx6CnLzyZiYPkw/vkxrgm9Q3y9RdvKBnM58qinWd7nZLmoJBo iOSIgfaLz9rpRlMcULWSKyRdtZjfGr9NEg== X-Google-Smtp-Source: ABdhPJzv6babQ+6BUXE5mVsGvFIl35hG1kpWPbUp3CIG01TzKmGbes7AmoWnuJwoY9/OuDMDaoCHbQ== X-Received: by 2002:a17:90b:4acd:: with SMTP id mh13mr4029747pjb.230.1637106930287; Tue, 16 Nov 2021 15:55:30 -0800 (PST) Received: from [192.168.2.139] ([50.53.98.191]) by smtp.googlemail.com with ESMTPSA id pc10sm950566pjb.9.2021.11.16.15.55.26 (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Tue, 16 Nov 2021 15:55:28 -0800 (PST) Subject: Re: SQL:2011 application time From: Paul Jungwirth To: Corey Huinker Cc: Jaime Casanova , PostgreSQL Hackers References: <20210904195556.GA10553@ahch-to> <20210911015017.GA8837@ahch-to> Message-ID: <5650d77a-1d73-fd8d-4410-8e97dbeff572@illuminatedcomputing.com> Date: Tue, 16 Nov 2021 15:55:26 -0800 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Thunderbird/78.14.0 MIME-Version: 1.0 In-Reply-To: Content-Type: multipart/mixed; boundary="------------AA1A9756402C5229DC7A97A9" Content-Language: en-US List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk This is a multi-part message in MIME format. --------------AA1A9756402C5229DC7A97A9 Content-Type: text/plain; charset=utf-8; format=flowed Content-Transfer-Encoding: 7bit Here are some new patches rebased on the latest master. I haven't made any substantive changes, but I should have time soon to take a stab at supporting partitioned tables and removing some of my own TODOs (things like making sure I'm locking things correctly). I don't think there is any outstanding feedback other than that. But in the meantime here are some up-to-date patches. Yours, -- Paul ~{:-) pj@illuminatedcomputing.com --------------AA1A9756402C5229DC7A97A9 Content-Type: text/x-patch; charset=UTF-8; name="v9-0001-Add-PERIODs.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="v9-0001-Add-PERIODs.patch" From bd3e2de65063c811aa30d0891cd4b125f1bda79f Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" Date: Sat, 9 Jan 2021 21:59:43 -0800 Subject: [PATCH v9 1/4] Add PERIODs - Added parsing for SQL:2011 syntax to define an application-time PERIOD on a table (in both CREATE TABLE and ALTER TABLE). Make sure we create the PERIOD after columns are known (since PERIODs can refer to them) but before constraints are handled (since PERIODs can appear in them). - Added ALTER TABLE DROP support for PERIODs. - Created postgres.pg_period table. - Created information_schema.periods view. - Added pg_dump support. - Added tests and documentation. - Automatically define a constraint for each PERIOD requiring the start column to be less than the end column. - When creating a PERIOD, choose an appropriate range type we can use to implement PERIOD-related operations. You can choose one explicitly if there is ambiguity (due to multiple range types created over the same base type). --- doc/src/sgml/catalogs.sgml | 132 ++++++ doc/src/sgml/ddl.sgml | 58 +++ doc/src/sgml/information_schema.sgml | 63 +++ doc/src/sgml/ref/alter_table.sgml | 25 + doc/src/sgml/ref/comment.sgml | 2 + doc/src/sgml/ref/create_table.sgml | 39 ++ src/backend/catalog/Makefile | 5 +- src/backend/catalog/aclchk.c | 2 + src/backend/catalog/dependency.c | 9 + src/backend/catalog/heap.c | 67 +++ src/backend/catalog/information_schema.sql | 24 +- src/backend/catalog/objectaddress.c | 73 +++ src/backend/catalog/pg_period.c | 106 +++++ src/backend/catalog/sql_features.txt | 2 +- src/backend/commands/alter.c | 1 + src/backend/commands/comment.c | 10 + src/backend/commands/event_trigger.c | 4 + src/backend/commands/seclabel.c | 1 + src/backend/commands/tablecmds.c | 510 ++++++++++++++++++++- src/backend/commands/view.c | 4 +- src/backend/nodes/copyfuncs.c | 19 + src/backend/nodes/equalfuncs.c | 17 + src/backend/nodes/nodeFuncs.c | 3 + src/backend/nodes/outfuncs.c | 45 ++ src/backend/parser/gram.y | 47 +- src/backend/parser/parse_utilcmd.c | 155 +++++++ src/backend/utils/cache/lsyscache.c | 87 ++++ src/backend/utils/cache/syscache.c | 34 +- src/bin/pg_dump/pg_backup_archiver.c | 1 + src/bin/pg_dump/pg_dump.c | 154 ++++++- src/bin/pg_dump/pg_dump.h | 15 + src/bin/pg_dump/pg_dump_sort.c | 7 + src/bin/psql/describe.c | 34 ++ src/include/catalog/dependency.h | 1 + src/include/catalog/heap.h | 4 + src/include/catalog/pg_index.h | 2 +- src/include/catalog/pg_period.h | 55 +++ src/include/catalog/pg_range.h | 1 + src/include/commands/tablecmds.h | 3 +- src/include/nodes/nodes.h | 1 + src/include/nodes/parsenodes.h | 31 +- src/include/parser/kwlist.h | 1 + src/include/parser/parse_utilcmd.h | 1 + src/include/utils/lsyscache.h | 3 + src/include/utils/syscache.h | 3 + src/test/regress/expected/periods.out | 79 ++++ src/test/regress/expected/sanity_check.out | 3 + src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/periods.sql | 62 +++ 49 files changed, 1970 insertions(+), 37 deletions(-) create mode 100644 src/backend/catalog/pg_period.c create mode 100644 src/include/catalog/pg_period.h create mode 100644 src/test/regress/expected/periods.out create mode 100644 src/test/regress/sql/periods.sql diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index c1d11be73f..e0d48a1463 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -225,6 +225,11 @@ information about partition key of tables + + pg_period + periods + + pg_policy row-security policies @@ -5525,6 +5530,133 @@ SCRAM-SHA-256$<iteration count>:&l are simple references. + + + + + + + + + <structname>pg_period</structname> + + + pg_period + + + + The catalog pg_period stores + information about system and application time periods. + + + + Periods are described in . + + + + <structname>pg_period</structname> Columns + + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + pername text + + + Period name + + + + + + perrelid oid + (references pg_class.oid) + + + The table this period belongs to + + + + + + perstart int2 + (references pg_attribute.attnum) + + + The number of the start column + + + + + + perend int2 + (references pg_attribute.attnum) + + + The number of the end column + + + + + + perrngtype oid + (references pg_type.oid) + + + The OID of the range type associated with this period + + + + + + perconstraint oid + (references pg_constraint.oid) + + + The OID of the period's CHECK constraint + + + + + + perislocal bool + + + This period is defined locally for the relation. Note that a period can + be locally defined and inherited simultaneously. + + + + + + perinhcount int4 + + + The number of direct inheritance ancestors this period has. A period + with a nonzero number of ancestors cannot be dropped. + + +
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 94f745aed0..1b8f524191 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1140,6 +1140,64 @@ CREATE TABLE circles (
+ + Periods + + + Periods are definitions on a table that associate a period name with a start + column and an end column. Both columns must be of exactly the same type + (including collation), have a btree operator class, and the start column + value must be strictly less than the end column value. + + + + There are two types of periods: application and system. System periods are + distinguished by their name which must be SYSTEM_TIME. Any + other name is an application period. + + + + Application Periods + + + period + application + + + + Application periods are defined on a table using the following syntax: + + + +CREATE TABLE billing_addresses ( + customer_id integer, + address_id integer, + valid_from date, + valid_to date, + PERIOD FOR validity (valid_from, valid_to) +); + + + + Application periods can be used to define temporal primary and foreign keys. + Any table with a temporal primary key is a temporal table and supports temporal update and delete commands. + + + + + System Periods + + + period + system + + + + Periods for SYSTEM_TIME are currently not implemented. + + + + System Columns diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml index c5e68c175f..925f3e9d4b 100644 --- a/doc/src/sgml/information_schema.sgml +++ b/doc/src/sgml/information_schema.sgml @@ -4171,6 +4171,69 @@ ORDER BY c.ordinal_position; + + <literal>periods</literal> + + + The view parameters contains information about the + periods of all tables in the current database. The start and end column + names are only shown if the current user has access to them (by way of being + the owner or having some privilege). + + + + <literal>periods</literal> Columns + + + + + Name + Data Type + Description + + + + + + table_catalog + sql_identifier + Name of the database containing the period (always the current database) + + + + table_schema + sql_identifier + Name of the schema containing the period + + + + table_name + sql_identifier + Name of the table containing the period + + + + period_name + sql_identifier + Name of the period + + + + start_column_name + sql_identifier + Name of the start column for the period + + + + end_column_name + sql_identifier + Name of the end column for the period + + + +
+
+ <literal>referential_constraints</literal> diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index bc5dcba59c..3521b679b3 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -60,6 +60,8 @@ ALTER TABLE [ IF EXISTS ] name ALTER CONSTRAINT constraint_name [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] VALIDATE CONSTRAINT constraint_name DROP CONSTRAINT [ IF EXISTS ] constraint_name [ RESTRICT | CASCADE ] + ADD PERIOD FOR period_name ( start_column, end_column ) [ WITH ( period_option = value [, ... ] ) ] + DROP PERIOD FOR period_name [ RESTRICT | CASCADE ] DISABLE TRIGGER [ trigger_name | ALL | USER ] ENABLE TRIGGER [ trigger_name | ALL | USER ] ENABLE REPLICA TRIGGER trigger_name @@ -566,6 +568,29 @@ WITH ( MODULUS numeric_literal, REM + + ADD PERIOD FOR + + + This form adds a new period to a table using the same syntax as + . + + + + + + DROP PERIOD FOR + + + This form drops the specified period on a table. The start and end + columns will not be dropped by this command but the + CHECK constraint will be. You will need to say + CASCADE if anything outside the table depends on the + column. + + + + DISABLE/ENABLE [ REPLICA | ALWAYS ] TRIGGER diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml index e07fc47fd3..2fffa99289 100644 --- a/doc/src/sgml/ref/comment.sgml +++ b/doc/src/sgml/ref/comment.sgml @@ -44,6 +44,7 @@ COMMENT ON OPERATOR operator_name (left_type, right_type) | OPERATOR CLASS object_name USING index_method | OPERATOR FAMILY object_name USING index_method | + PERIOD relation_name.period_name | POLICY policy_name ON table_name | [ PROCEDURAL ] LANGUAGE object_name | PROCEDURE procedure_name [ ( [ [ argmode ] [ argname ] argtype [, ...] ] ) ] | @@ -327,6 +328,7 @@ COMMENT ON OPERATOR ^ (text, text) IS 'Performs intersection of two texts'; COMMENT ON OPERATOR - (NONE, integer) IS 'Unary minus'; COMMENT ON OPERATOR CLASS int4ops USING btree IS '4 byte integer operators for btrees'; COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'all integer operators for btrees'; +COMMENT ON PERIOD my_table.my_column IS 'Sales promotion validity'; COMMENT ON POLICY my_policy ON mytable IS 'Filter rows by users'; COMMENT ON PROCEDURE my_proc (integer, integer) IS 'Runs a report'; COMMENT ON PUBLICATION alltables IS 'Publishes all operations on all tables'; diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 57d51a676a..7d3ef82f6a 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -23,6 +23,7 @@ PostgreSQL documentation CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name ( [ { column_name data_type [ COMPRESSION compression_method ] [ COLLATE collation ] [ column_constraint [ ... ] ] + | period_definition | table_constraint | LIKE source_table [ like_option ... ] } [, ... ] @@ -37,6 +38,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name OF type_name [ ( { column_name [ WITH OPTIONS ] [ column_constraint [ ... ] ] + | period_definition | table_constraint } [, ... ] ) ] @@ -49,6 +51,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name PARTITION OF parent_table [ ( { column_name [ WITH OPTIONS ] [ column_constraint [ ... ] ] + | period_definition | table_constraint } [, ... ] ) ] { FOR VALUES partition_bound_spec | DEFAULT } @@ -73,6 +76,11 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXI [ ON DELETE referential_action ] [ ON UPDATE referential_action ] } [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] +and period_definition is: + +PERIOD FOR { period_name | SYSTEM_TIME } ( column_name, column_name ) +[ WITH ( period_option = value [, ... ] ) ] + and table_constraint is: [ CONSTRAINT constraint_name ] @@ -139,6 +147,14 @@ WITH ( MODULUS numeric_literal, REM name as any existing data type in the same schema. + + Periods my be defined on tables, specifying that two existing columns + represent start and end values for the period. Periods may have any name + that doesn't conflict with a column name, but the name + SYSTEM_TIME is special, used for versioning tables. + System periods are not yet implemented. See for more details. + + The optional constraint clauses specify constraints (tests) that new or updated rows must satisfy for an insert or update operation @@ -753,6 +769,29 @@ WITH ( MODULUS numeric_literal, REM + + PERIOD FOR period_name ( column_name, column_name ) [ WITH ( period_option = value [, ... ] ) ] + + + A period definition gives semantic meaning to two existing columns of + the table. It defines a "start column" and an "end column" where the + start value is strictly less than the end value. A + CHECK constraint is automatically created to enforce + this. + + + + Both columns must have exactly the same type and must have a range type + defined from their base type. If there are several range types for that + base type, you must specify which one you want by using the + rangetype period_option. + Any base type is allowed, as long as it has a range type, although it is + expected that most periods will use temporal types like timestamptz + or date. + + + + CONSTRAINT constraint_name diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index 4e6efda97f..cef4bf26e8 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -37,6 +37,7 @@ OBJS = \ pg_largeobject.o \ pg_namespace.o \ pg_operator.o \ + pg_period.o \ pg_proc.o \ pg_publication.o \ pg_range.o \ @@ -67,8 +68,8 @@ CATALOG_HEADERS := \ pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \ pg_foreign_table.h pg_policy.h pg_replication_origin.h \ pg_default_acl.h pg_init_privs.h pg_seclabel.h pg_shseclabel.h \ - pg_collation.h pg_partitioned_table.h pg_range.h pg_transform.h \ - pg_sequence.h pg_publication.h pg_publication_namespace.h \ + pg_collation.h pg_partitioned_table.h pg_period.h pg_range.h \ + pg_transform.h pg_sequence.h pg_publication.h pg_publication_namespace.h \ pg_publication_rel.h pg_subscription.h pg_subscription_rel.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index ce0a4ff14e..c7e752ac8c 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -3427,6 +3427,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_DEFAULT: case OBJECT_DEFACL: case OBJECT_DOMCONSTRAINT: + case OBJECT_PERIOD: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_ROLE: @@ -3567,6 +3568,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype, case OBJECT_DEFAULT: case OBJECT_DEFACL: case OBJECT_DOMCONSTRAINT: + case OBJECT_PERIOD: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: case OBJECT_ROLE: diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index fe9c714257..558b5afb99 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -46,6 +46,7 @@ #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" #include "catalog/pg_opfamily.h" +#include "catalog/pg_period.h" #include "catalog/pg_policy.h" #include "catalog/pg_proc.h" #include "catalog/pg_publication.h" @@ -151,6 +152,7 @@ static const Oid object_classes[] = { CastRelationId, /* OCLASS_CAST */ CollationRelationId, /* OCLASS_COLLATION */ ConstraintRelationId, /* OCLASS_CONSTRAINT */ + PeriodRelationId, /* OCLASS_PERIOD */ ConversionRelationId, /* OCLASS_CONVERSION */ AttrDefaultRelationId, /* OCLASS_DEFAULT */ LanguageRelationId, /* OCLASS_LANGUAGE */ @@ -1431,6 +1433,10 @@ doDeletion(const ObjectAddress *object, int flags) RemoveConstraintById(object->objectId); break; + case OCLASS_PERIOD: + RemovePeriodById(object->objectId); + break; + case OCLASS_DEFAULT: RemoveAttrDefaultById(object->objectId); break; @@ -2781,6 +2787,9 @@ getObjectClass(const ObjectAddress *object) case ConstraintRelationId: return OCLASS_CONSTRAINT; + case PeriodRelationId: + return OCLASS_PERIOD; + case ConversionRelationId: return OCLASS_CONVERSION; diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 81cc39fb70..49cb627f5e 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -56,6 +56,7 @@ #include "catalog/pg_namespace.h" #include "catalog/pg_opclass.h" #include "catalog/pg_partitioned_table.h" +#include "catalog/pg_period.h" #include "catalog/pg_statistic.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_tablespace.h" @@ -2203,6 +2204,72 @@ SetAttrMissing(Oid relid, char *attname, char *value) table_close(tablerel, AccessExclusiveLock); } +/* + * Store a period of relation rel. + * + * Returns the OID of the new pg_period tuple. + */ +Oid +StorePeriod(Relation rel, const char *periodname, AttrNumber startnum, + AttrNumber endnum, Oid rngtypid, Oid conoid) +{ + Datum values[Natts_pg_period]; + bool nulls[Natts_pg_period]; + Relation pg_period; + HeapTuple tuple; + Oid oid; + NameData pername; + ObjectAddress myself, referenced; + + namestrcpy(&pername, periodname); + + MemSet(values, 0, sizeof(values)); + MemSet(nulls, false, sizeof(nulls)); + + pg_period = table_open(PeriodRelationId, RowExclusiveLock); + + oid = GetNewOidWithIndex(pg_period, AttrDefaultOidIndexId, Anum_pg_period_oid); + values[Anum_pg_period_oid - 1] = ObjectIdGetDatum(oid); + values[Anum_pg_period_pername - 1] = NameGetDatum(&pername); + values[Anum_pg_period_perrelid - 1] = RelationGetRelid(rel); + values[Anum_pg_period_perstart - 1] = startnum; + values[Anum_pg_period_perend - 1] = endnum; + values[Anum_pg_period_perrngtype - 1] = rngtypid; + values[Anum_pg_period_perconstraint - 1] = conoid; + values[Anum_pg_period_perislocal - 1] = true; + values[Anum_pg_period_perinhcount - 1] = 0; + + tuple = heap_form_tuple(RelationGetDescr(pg_period), values, nulls); + CatalogTupleInsert(pg_period, tuple); + + ObjectAddressSet(myself, PeriodRelationId, oid); + + /* Drop the period when the table is dropped. */ + ObjectAddressSet(referenced, RelationRelationId, RelationGetRelid(rel)); + recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO); + + /* Forbid dropping the columns of the period. */ + ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), startnum); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + ObjectAddressSubSet(referenced, RelationRelationId, RelationGetRelid(rel), endnum); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + /* Make sure we don't lose our rangetype. */ + ObjectAddressSet(referenced, TypeRelationId, rngtypid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + /* + * The constraint is an implementation detail, so we mark it as such. + * (Note that myself and referenced are reversed for this one.) + */ + ObjectAddressSet(referenced, ConstraintRelationId, conoid); + recordDependencyOn(&referenced, &myself, DEPENDENCY_INTERNAL); + + table_close(pg_period, RowExclusiveLock); + + return oid; +} + /* * Store a default expression for column attnum of relation rel. * diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql index 11d9dd60c2..5758703f7a 100644 --- a/src/backend/catalog/information_schema.sql +++ b/src/backend/catalog/information_schema.sql @@ -1198,7 +1198,29 @@ GRANT SELECT ON parameters TO PUBLIC; * PERIODS view */ --- feature not supported +CREATE VIEW periods AS + SELECT current_database()::information_schema.sql_identifier AS table_catalog, + nc.nspname::information_schema.sql_identifier AS table_schema, + c.relname::information_schema.sql_identifier AS table_name, + p.pername::information_schema.sql_identifier AS period_name, + CASE WHEN pg_has_role(c.relowner, 'USAGE') + OR has_column_privilege(sa.attrelid, sa.attnum, 'SELECT, INSERT, UPDATE, REFERENCES') + THEN sa.attname::information_schema.sql_identifier + END AS start_column_name, + CASE WHEN pg_has_role(c.relowner, 'USAGE') + OR has_column_privilege(ea.attrelid, ea.attnum, 'SELECT, INSERT, UPDATE, REFERENCES') + THEN ea.attname::information_schema.sql_identifier + END AS end_column_name + FROM pg_period AS p + JOIN pg_class AS c ON c.oid = p.perrelid + JOIN pg_namespace AS nc ON nc.oid = c.relnamespace + JOIN pg_attribute AS sa ON (sa.attrelid, sa.attnum) = (p.perrelid, p.perstart) + JOIN pg_attribute AS ea ON (ea.attrelid, ea.attnum) = (p.perrelid, p.perend) + WHERE NOT pg_is_other_temp_schema(nc.oid) + --AND c.relkind = ANY (ARRAY['r']); + AND c.relkind IN ('r', 'v'); + +GRANT SELECT ON periods TO PUBLIC; /* diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c index 2bae3fbb17..8b714b980c 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -45,6 +45,7 @@ #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" #include "catalog/pg_opfamily.h" +#include "catalog/pg_period.h" #include "catalog/pg_policy.h" #include "catalog/pg_proc.h" #include "catalog/pg_publication.h" @@ -714,6 +715,10 @@ static const struct object_type_map { "domain constraint", OBJECT_DOMCONSTRAINT }, + /* OCLASS_PERIOD */ + { + "period", OBJECT_PERIOD + }, /* OCLASS_CONVERSION */ { "conversion", OBJECT_CONVERSION @@ -983,6 +988,7 @@ get_object_address(ObjectType objtype, Node *object, case OBJECT_TRIGGER: case OBJECT_TABCONSTRAINT: case OBJECT_POLICY: + case OBJECT_PERIOD: address = get_object_address_relobject(objtype, castNode(List, object), &relation, missing_ok); break; @@ -1478,6 +1484,13 @@ get_object_address_relobject(ObjectType objtype, List *object, InvalidOid; address.objectSubId = 0; break; + case OBJECT_PERIOD: + address.classId = PeriodRelationId; + address.objectId = relation ? + get_relation_period_oid(reloid, depname, missing_ok) : + InvalidOid; + address.objectSubId = 0; + break; default: elog(ERROR, "unrecognized objtype: %d", (int) objtype); } @@ -2320,6 +2333,7 @@ pg_get_object_address(PG_FUNCTION_ARGS) case OBJECT_RULE: case OBJECT_TRIGGER: case OBJECT_TABCONSTRAINT: + case OBJECT_PERIOD: case OBJECT_OPCLASS: case OBJECT_OPFAMILY: objnode = (Node *) name; @@ -2435,6 +2449,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_TRIGGER: case OBJECT_POLICY: case OBJECT_TABCONSTRAINT: + case OBJECT_PERIOD: if (!pg_class_ownercheck(RelationGetRelid(relation), roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, objtype, RelationGetRelationName(relation)); @@ -3128,6 +3143,38 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok) break; } + case OCLASS_PERIOD: + { + HeapTuple perTup; + Form_pg_period per; + + perTup = SearchSysCache1(PERIODOID, + ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(perTup)) + elog(ERROR, "cache lookup failed for period %u", + object->objectId); + per = (Form_pg_period) GETSTRUCT(perTup); + + if (OidIsValid(per->perrelid)) + { + StringInfoData rel; + + initStringInfo(&rel); + getRelationDescription(&rel, per->perrelid, false); + appendStringInfo(&buffer, _("period %s on %s"), + NameStr(per->pername), rel.data); + pfree(rel.data); + } + else + { + appendStringInfo(&buffer, _("period %s"), + NameStr(per->pername)); + } + + ReleaseSysCache(perTup); + break; + } + case OCLASS_CONVERSION: { HeapTuple conTup; @@ -4478,6 +4525,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok) missing_ok); break; + case OCLASS_PERIOD: + appendStringInfoString(&buffer, "period"); + break; + case OCLASS_CONVERSION: appendStringInfoString(&buffer, "conversion"); break; @@ -4977,6 +5028,28 @@ getObjectIdentityParts(const ObjectAddress *object, break; } + case OCLASS_PERIOD: + { + HeapTuple perTup; + Form_pg_period per; + + perTup = SearchSysCache1(PERIODOID, + ObjectIdGetDatum(object->objectId)); + if (!HeapTupleIsValid(perTup)) + elog(ERROR, "cache lookup failed for period %u", + object->objectId); + per = (Form_pg_period) GETSTRUCT(perTup); + + appendStringInfo(&buffer, "%s on ", + quote_identifier(NameStr(per->pername))); + getRelationIdentity(&buffer, per->perrelid, objname, false); + if (objname) + *objname = lappend(*objname, pstrdup(NameStr(per->pername))); + + ReleaseSysCache(perTup); + break; + } + case OCLASS_CONVERSION: { HeapTuple conTup; diff --git a/src/backend/catalog/pg_period.c b/src/backend/catalog/pg_period.c new file mode 100644 index 0000000000..40ec3fbdbd --- /dev/null +++ b/src/backend/catalog/pg_period.c @@ -0,0 +1,106 @@ +/*------------------------------------------------------------------------- + * + * pg_period.c + * routines to support manipulation of the pg_period relation + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * + * + * IDENTIFICATION + * src/backend/catalog/pg_period.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/heapam.h" +#include "access/htup_details.h" +#include "catalog/indexing.h" +#include "catalog/pg_period.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" +#include "utils/syscache.h" + + +/* + * Delete a single period record. + */ +void +RemovePeriodById(Oid periodId) +{ + Relation pg_period; + HeapTuple tup; + + pg_period = table_open(PeriodRelationId, RowExclusiveLock); + + tup = SearchSysCache1(PERIODOID, ObjectIdGetDatum(periodId)); + if (!HeapTupleIsValid(tup)) /* should not happen */ + elog(ERROR, "cache lookup failed for period %u", periodId); + + /* Fry the period itself */ + CatalogTupleDelete(pg_period, &tup->t_self); + + /* Clean up */ + ReleaseSysCache(tup); + table_close(pg_period, RowExclusiveLock); +} + +/* + * get_relation_period_oid + * Find a period on the specified relation with the specified name. + * Returns period's OID. + */ +Oid +get_relation_period_oid(Oid relid, const char *pername, bool missing_ok) +{ + Relation pg_period; + HeapTuple tuple; + SysScanDesc scan; + ScanKeyData skey[1]; + Oid perOid = InvalidOid; + + /* + * Fetch the period tuple from pg_period. There may be more than + * one match, because periods are not required to have unique names; + * if so, error out. + */ + pg_period = table_open(PeriodRelationId, AccessShareLock); + + ScanKeyInit(&skey[0], + Anum_pg_period_perrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + + scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, true, + NULL, 1, skey); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_period period = (Form_pg_period) GETSTRUCT(tuple); + + if (strcmp(NameStr(period->pername), pername) == 0) + { + if (OidIsValid(perOid)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("table \"%s\" has multiple periods named \"%s\"", + get_rel_name(relid), pername))); + perOid = period->oid; + } + } + + systable_endscan(scan); + + /* If no such period exists, complain */ + if (!OidIsValid(perOid) && !missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("period \"%s\" for table \"%s\" does not exist", + pername, get_rel_name(relid)))); + + table_close(pg_period, AccessShareLock); + + return perOid; +} diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index 9f424216e2..9662d713c9 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -438,7 +438,7 @@ T176 Sequence generator support NO supported except for NEXT VALUE FOR T177 Sequence generator support: simple restart option YES T178 Identity columns: simple restart option YES T180 System-versioned tables NO -T181 Application-time period tables NO +T181 Application-time period tables YES T191 Referential action RESTRICT YES T201 Comparable data types for referential constraints YES T211 Basic trigger capability NO diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c index 40044070cf..6b2efae03f 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -640,6 +640,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid, case OCLASS_CAST: case OCLASS_CONSTRAINT: + case OCLASS_PERIOD: case OCLASS_DEFAULT: case OCLASS_LANGUAGE: case OCLASS_LARGEOBJECT: diff --git a/src/backend/commands/comment.c b/src/backend/commands/comment.c index d4943e374a..ea59c2fa04 100644 --- a/src/backend/commands/comment.c +++ b/src/backend/commands/comment.c @@ -102,6 +102,16 @@ CommentObject(CommentStmt *stmt) RelationGetRelationName(relation)), errdetail_relkind_not_supported(relation->rd_rel->relkind))); break; + + case OBJECT_PERIOD: + /* Periods can only go on tables */ + if (relation->rd_rel->relkind != RELKIND_RELATION) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table", + RelationGetRelationName(relation)))); + break; + default: break; } diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index df264329d8..bd0804ea1c 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -970,6 +970,7 @@ EventTriggerSupportsObjectType(ObjectType obtype) case OBJECT_OPCLASS: case OBJECT_OPERATOR: case OBJECT_OPFAMILY: + case OBJECT_PERIOD: case OBJECT_POLICY: case OBJECT_PROCEDURE: case OBJECT_PUBLICATION: @@ -1026,6 +1027,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass) case OCLASS_CAST: case OCLASS_COLLATION: case OCLASS_CONSTRAINT: + case OCLASS_PERIOD: case OCLASS_CONVERSION: case OCLASS_DEFAULT: case OCLASS_LANGUAGE: @@ -2126,6 +2128,7 @@ stringify_grant_objtype(ObjectType objtype) case OBJECT_OPCLASS: case OBJECT_OPERATOR: case OBJECT_OPFAMILY: + case OBJECT_PERIOD: case OBJECT_POLICY: case OBJECT_PUBLICATION: case OBJECT_PUBLICATION_NAMESPACE: @@ -2209,6 +2212,7 @@ stringify_adefprivs_objtype(ObjectType objtype) case OBJECT_OPCLASS: case OBJECT_OPERATOR: case OBJECT_OPFAMILY: + case OBJECT_PERIOD: case OBJECT_POLICY: case OBJECT_PUBLICATION: case OBJECT_PUBLICATION_NAMESPACE: diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c index 53c18628a7..eea5de2750 100644 --- a/src/backend/commands/seclabel.c +++ b/src/backend/commands/seclabel.c @@ -78,6 +78,7 @@ SecLabelSupportsObjectType(ObjectType objtype) case OBJECT_OPCLASS: case OBJECT_OPERATOR: case OBJECT_OPFAMILY: + case OBJECT_PERIOD: case OBJECT_POLICY: case OBJECT_PUBLICATION_NAMESPACE: case OBJECT_PUBLICATION_REL: diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index d675d261f7..745e78cdcd 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -40,6 +40,7 @@ #include "catalog/pg_inherits.h" #include "catalog/pg_namespace.h" #include "catalog/pg_opclass.h" +#include "catalog/pg_period.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_statistic_ext.h" #include "catalog/pg_trigger.h" @@ -144,13 +145,14 @@ static List *on_commits = NIL; #define AT_PASS_OLD_CONSTR 3 /* re-add existing constraints */ /* We could support a RENAME COLUMN pass here, but not currently used */ #define AT_PASS_ADD_COL 4 /* ADD COLUMN */ -#define AT_PASS_ADD_CONSTR 5 /* ADD constraints (initial examination) */ -#define AT_PASS_COL_ATTRS 6 /* set column attributes, eg NOT NULL */ -#define AT_PASS_ADD_INDEXCONSTR 7 /* ADD index-based constraints */ -#define AT_PASS_ADD_INDEX 8 /* ADD indexes */ -#define AT_PASS_ADD_OTHERCONSTR 9 /* ADD other constraints, defaults */ -#define AT_PASS_MISC 10 /* other stuff */ -#define AT_NUM_PASSES 11 +#define AT_PASS_ADD_PERIOD 5 /* ADD PERIOD */ +#define AT_PASS_ADD_CONSTR 6 /* ADD constraints (initial examination) */ +#define AT_PASS_COL_ATTRS 7 /* set column attributes, eg NOT NULL */ +#define AT_PASS_ADD_INDEXCONSTR 8 /* ADD index-based constraints */ +#define AT_PASS_ADD_INDEX 9 /* ADD indexes */ +#define AT_PASS_ADD_OTHERCONSTR 10 /* ADD other constraints, defaults */ +#define AT_PASS_MISC 11 /* other stuff */ +#define AT_NUM_PASSES 12 typedef struct AlteredTableInfo { @@ -418,6 +420,8 @@ static ObjectAddress ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, AlterTableUtilityContext *context); static bool check_for_column_name_collision(Relation rel, const char *colname, bool if_not_exists); +static bool check_for_period_name_collision(Relation rel, const char *pername, + bool if_not_exists); static void add_column_datatype_dependency(Oid relid, int32 attnum, Oid typid); static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid); static void ATPrepDropNotNull(Relation rel, bool recurse, bool recursing); @@ -437,6 +441,12 @@ static ObjectAddress ATExecColumnDefault(Relation rel, const char *colName, Node *newDefault, LOCKMODE lockmode); static ObjectAddress ATExecCookedColumnDefault(Relation rel, AttrNumber attnum, Node *newDefault); +static ObjectAddress ATExecAddPeriod(Relation rel, Period *period, LOCKMODE lockmode, + AlterTableUtilityContext *context); +static void ATExecDropPeriod(Relation rel, const char *periodName, + DropBehavior behavior, + bool recurse, bool recursing, + bool missing_ok, LOCKMODE lockmode); static ObjectAddress ATExecAddIdentity(Relation rel, const char *colName, Node *def, LOCKMODE lockmode); static ObjectAddress ATExecSetIdentity(Relation rel, const char *colName, @@ -602,6 +612,7 @@ static void refuseDupeIndexAttach(Relation parentIdx, Relation partIdx, static List *GetParentedForeignKeyRefs(Relation partition); static void ATDetachCheckNoForeignKeyRefs(Relation partition); static char GetAttributeCompression(Oid atttypid, char *compression); +static void AddRelationNewPeriod(Relation rel, Period *period); /* ---------------------------------------------------------------- @@ -1213,6 +1224,17 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, AddRelationNewConstraints(rel, NIL, stmt->constraints, true, true, false, queryString); + /* + * Create periods for the table. This must come after we create columns + * and before we create index constraints. It will automatically create + * NOT NULL and CHECK constraints for the period. + */ + foreach(listptr, stmt->periods) + { + Period *period = (Period *) lfirst(listptr); + AddRelationNewPeriod(rel, period); + } + ObjectAddressSet(address, RelationRelationId, relationId); /* @@ -1224,6 +1246,161 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, return address; } +static Constraint * +make_period_not_backward(Relation rel, Period *period, char *constraintname) +{ + ColumnRef *scol, *ecol; + Constraint *constr; + + if (constraintname == NULL) + constraintname = ChooseConstraintName(RelationGetRelationName(rel), + period->periodname, + "check", + RelationGetNamespace(rel), + NIL); + period->constraintname = constraintname; + + scol = makeNode(ColumnRef); + scol->fields = list_make1(makeString(pstrdup(period->startcolname))); + scol->location = 0; + + ecol = makeNode(ColumnRef); + ecol->fields = list_make1(makeString(pstrdup(period->endcolname))); + ecol->location = 0; + + constr = makeNode(Constraint); + constr->contype = CONSTR_CHECK; + constr->conname = constraintname; + constr->deferrable = false; + constr->initdeferred = false; + constr->location = -1; + constr->is_no_inherit = false; + constr->raw_expr = (Node *) makeSimpleA_Expr(AEXPR_OP, "<", (Node *) scol, (Node *) ecol, 0); + constr->cooked_expr = NULL; + constr->skip_validation = false; + constr->initially_valid = true; + + return constr; +} + +static void +validate_period(Relation rel, Period *period, HeapTuple *starttuple, HeapTuple *endtuple, Relation *attrelation, Form_pg_attribute *startatttuple, AttrNumber *startattnum, AttrNumber *endattnum) +{ + Form_pg_attribute endatttuple; + *attrelation = table_open(AttributeRelationId, RowExclusiveLock); + + /* Find the start column */ + *starttuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->startcolname); + if (!HeapTupleIsValid(*starttuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + period->startcolname, RelationGetRelationName(rel)))); + *startatttuple = (Form_pg_attribute) GETSTRUCT(*starttuple); + *startattnum = (*startatttuple)->attnum; + + /* Make sure it's not a system column */ + if (*startattnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use system column \"%s\" in period", + period->startcolname))); + + /* Find the end column */ + *endtuple = SearchSysCacheCopyAttName(RelationGetRelid(rel), period->endcolname); + if (!HeapTupleIsValid(*endtuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + period->endcolname, RelationGetRelationName(rel)))); + endatttuple = (Form_pg_attribute) GETSTRUCT(*endtuple); + *endattnum = endatttuple->attnum; + + /* Make sure it's not a system column */ + if (*endattnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), + errmsg("cannot use system column \"%s\" in period", + period->endcolname))); + + /* Both columns must be of same type */ + if ((*startatttuple)->atttypid != endatttuple->atttypid) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("start and end columns of period must be of same type"))); + + /* Both columns must have the same collation */ + if ((*startatttuple)->attcollation != endatttuple->attcollation) + ereport(ERROR, + (errcode(ERRCODE_COLLATION_MISMATCH), + errmsg("start and end columns of period must have same collation"))); +} + +/* + * make_constraint_for_period + * + * Add constraints to make both columns NOT NULL and CHECK (start < end). + * + * Returns the CHECK constraint Oid. + */ +static Oid +make_constraint_for_period(Relation rel, Period *period, char *constraintname, + LOCKMODE lockmode, AlterTableUtilityContext *context) +{ + List *cmds = NIL; + AlterTableCmd *cmd; + Constraint *constr; + + constr = make_period_not_backward(rel, period, constraintname); + cmd = makeNode(AlterTableCmd); + cmd->subtype = AT_AddConstraint; + cmd->def = (Node *) constr; + cmds = lappend(cmds, cmd); + + /* Do the deed. */ + AlterTableInternal(RelationGetRelid(rel), cmds, true, context); + + return get_relation_constraint_oid(RelationGetRelid(rel), period->constraintname, false); +} + +static void +AddRelationNewPeriod(Relation rel, Period *period) +{ + Relation attrelation; + HeapTuple starttuple, endtuple; + Form_pg_attribute startatttuple; + AttrNumber startattnum, endattnum; + Oid conoid; + Constraint *constr; + List *newconstrs; + + /* The period name must not already exist */ + (void) check_for_period_name_collision(rel, period->periodname, false); + + validate_period(rel, period, &starttuple, &endtuple, &attrelation, &startatttuple, &startattnum, &endattnum); + + heap_freetuple(starttuple); + heap_freetuple(endtuple); + + if (period->constraintname == NULL) + { + period->constraintname = ChooseConstraintName(RelationGetRelationName(rel), + period->periodname, + "check", + RelationGetNamespace(rel), + NIL); + } + + constr = make_period_not_backward(rel, period, period->constraintname); + newconstrs = AddRelationNewConstraints(rel, NIL, list_make1(constr), false, true, true, NULL); + conoid = ((CookedConstraint *) linitial(newconstrs))->conoid; + + /* Save it */ + StorePeriod(rel, period->periodname, startattnum, endattnum, period->rngtypid, conoid); + + table_close(attrelation, RowExclusiveLock); +} + /* * Emit the right error or warning message for a "DROP" command issued on a * non-existent relation @@ -2284,6 +2461,8 @@ MergeAttributes(List *schema, List *supers, char relpersistence, * * Note that we also need to check that we do not exceed this figure after * including columns from inherited relations. + * + * TODO: What about periods? */ if (list_length(schema) > MaxHeapAttributeNumber) ereport(ERROR, @@ -2368,6 +2547,8 @@ MergeAttributes(List *schema, List *supers, char relpersistence, * Scan the parents left-to-right, and merge their attributes to form a * list of inherited attributes (inhSchema). Also check to see if we need * to inherit an OID column. + * + * TODO: probably need periods here, too. */ child_attno = 0; foreach(entry, supers) @@ -4048,12 +4229,12 @@ AlterTable(AlterTableStmt *stmt, LOCKMODE lockmode, * existing query plans. On the assumption it's not used for such, we * don't have to reject pending AFTER triggers, either. * - * Also, since we don't have an AlterTableUtilityContext, this cannot be + * Also, if you don't pass an AlterTableUtilityContext, this cannot be * used for any subcommand types that require parse transformation or * could generate subcommands that have to be passed to ProcessUtility. */ void -AlterTableInternal(Oid relid, List *cmds, bool recurse) +AlterTableInternal(Oid relid, List *cmds, bool recurse, AlterTableUtilityContext *context) { Relation rel; LOCKMODE lockmode = AlterTableGetLockLevel(cmds); @@ -4062,7 +4243,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse) EventTriggerAlterTableRelid(relid); - ATController(NULL, rel, cmds, recurse, lockmode, NULL); + ATController(NULL, rel, cmds, recurse, lockmode, context); } /* @@ -4124,6 +4305,20 @@ AlterTableGetLockLevel(List *cmds) cmd_lockmode = AccessExclusiveLock; break; + /* + * Adding a period may conflict with a column name so we need + * an exclusive lock to make sure columns aren't added + * concurrently. + * + * It also adds a CHECK constraint so we need to match that + * level, and dropping a period drops the constraint so that + * level needs to be matched, too. + */ + case AT_AddPeriod: + case AT_DropPeriod: + cmd_lockmode = AccessExclusiveLock; + break; + /* * These subcommands may require addition of toast tables. If * we add a toast table to a table currently being scanned, we @@ -4479,6 +4674,18 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, /* This command never recurses */ pass = AT_PASS_ADD_OTHERCONSTR; break; + case AT_AddPeriod: /* ALTER TABLE ... ADD PERIOD FOR name (start, end) */ + ATSimplePermissions(cmd->subtype, rel, ATT_TABLE); + /* + * We must add PERIODs after columns, in case they reference a newly-added column, + * and before constraints, in case a newly-added PK/FK references them. + */ + pass = AT_PASS_ADD_PERIOD; + break; + case AT_DropPeriod: /* ALTER TABLE ... DROP PERIOD FOR name */ + ATSimplePermissions(cmd->subtype, rel, ATT_TABLE); + pass = AT_PASS_DROP; + break; case AT_AddIdentity: ATSimplePermissions(cmd->subtype, rel, ATT_TABLE | ATT_VIEW | ATT_FOREIGN_TABLE); /* This command never recurses */ @@ -4878,6 +5085,14 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, case AT_CookedColumnDefault: /* add a pre-cooked default */ address = ATExecCookedColumnDefault(rel, cmd->num, cmd->def); break; + case AT_AddPeriod: + address = ATExecAddPeriod(rel, (Period *) cmd->def, lockmode, context); + break; + case AT_DropPeriod: + ATExecDropPeriod(rel, cmd->name, cmd->behavior, + false, false, + cmd->missing_ok, lockmode); + break; case AT_AddIdentity: cmd = ATParseTransformCmd(wqueue, tab, rel, cmd, false, lockmode, cur_pass, context); @@ -6008,6 +6223,8 @@ alter_table_type_to_string(AlterTableType cmdtype) case AT_AddColumnRecurse: case AT_AddColumnToView: return "ADD COLUMN"; + case AT_AddPeriod: + return "ADD PERIOD"; case AT_ColumnDefault: case AT_CookedColumnDefault: return "ALTER COLUMN ... SET DEFAULT"; @@ -6032,6 +6249,8 @@ alter_table_type_to_string(AlterTableType cmdtype) case AT_DropColumn: case AT_DropColumnRecurse: return "DROP COLUMN"; + case AT_DropPeriod: + return "DROP PERIOD"; case AT_AddIndex: case AT_ReAddIndex: return NULL; /* not real grammar */ @@ -6986,14 +7205,29 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, /* * If a new or renamed column will collide with the name of an existing * column and if_not_exists is false then error out, else do nothing. + * + * See also check_for_period_name_collision. */ static bool check_for_column_name_collision(Relation rel, const char *colname, bool if_not_exists) { - HeapTuple attTuple; + HeapTuple attTuple, perTuple; int attnum; + /* If the name exists as a period, we're done. */ + perTuple = SearchSysCache2(PERIODNAME, + ObjectIdGetDatum(RelationGetRelid(rel)), + PointerGetDatum(colname)); + if (HeapTupleIsValid(perTuple)) + { + ReleaseSysCache(perTuple); + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("column name \"%s\" conflicts with a period name", + colname))); + } + /* * this test is deliberately not attisdropped-aware, since if one tries to * add a column matching a dropped column name, it's gonna fail anyway. @@ -7037,6 +7271,78 @@ check_for_column_name_collision(Relation rel, const char *colname, return true; } +/* + * If a new period name will collide with the name of an existing column or + * period [and if_not_exists is false] then error out, else do nothing. + * + * See also check_for_column_name_collision. + */ +static bool +check_for_period_name_collision(Relation rel, const char *pername, + bool if_not_exists) +{ + HeapTuple attTuple, perTuple; + int attnum; + + /* TODO: implement IF [NOT] EXISTS for periods */ + Assert(!if_not_exists); + + /* If there is already a period with this name, then we're done. */ + perTuple = SearchSysCache2(PERIODNAME, + ObjectIdGetDatum(RelationGetRelid(rel)), + PointerGetDatum(pername)); + if (HeapTupleIsValid(perTuple)) + { + if (if_not_exists) + { + ReleaseSysCache(perTuple); + + ereport(NOTICE, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("period \"%s\" of relation \"%s\" already exists, skipping", + pername, RelationGetRelationName(rel)))); + return false; + } + + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("period \"%s\" of relation \"%s\" already exists", + pername, RelationGetRelationName(rel)))); + } + + /* + * this test is deliberately not attisdropped-aware, since if one tries to + * add a column matching a dropped column name, it's gonna fail anyway. + * + * XXX: Does this hold for periods? + */ + attTuple = SearchSysCache2(ATTNAME, + ObjectIdGetDatum(RelationGetRelid(rel)), + PointerGetDatum(pername)); + if (HeapTupleIsValid(attTuple)) + { + attnum = ((Form_pg_attribute) GETSTRUCT(attTuple))->attnum; + ReleaseSysCache(attTuple); + + /* + * We throw a different error message for conflicts with system column + * names, since they are normally not shown and the user might otherwise + * be confused about the reason for the conflict. + */ + if (attnum <= 0) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("period name \"%s\" conflicts with a system column name", + pername))); + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("period name \"%s\" conflicts with a column name", + pername))); + } + + return true; +} + /* * Install a column's dependency on its datatype. */ @@ -7552,6 +7858,177 @@ ATExecCookedColumnDefault(Relation rel, AttrNumber attnum, return address; } +/* + * ALTER TABLE ADD PERIOD + * + * Return the address of the period. + */ +static ObjectAddress +ATExecAddPeriod(Relation rel, Period *period, LOCKMODE lockmode, + AlterTableUtilityContext *context) +{ + Relation attrelation; + HeapTuple starttuple, endtuple; + Form_pg_attribute startatttuple; + AttrNumber startattnum, endattnum; + Oid coltypid, rngtypid, conoid, periodoid; + ObjectAddress address = InvalidObjectAddress; + + /* + * PERIOD FOR SYSTEM_TIME is not yet implemented, but make sure no one uses + * the name. + */ + if (strcmp(period->periodname, "system_time") == 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("PERIOD FOR SYSTEM_TIME is not supported"))); + + /* The period name must not already exist */ + (void) check_for_period_name_collision(rel, period->periodname, false); + + /* Parse options */ + transformPeriodOptions(period); + + validate_period(rel, period, &starttuple, &endtuple, &attrelation, &startatttuple, &startattnum, &endattnum); + + /* + * Find a suitable range type for operations involving this period. + * Use the rangetype option if provided, otherwise try to find a + * non-ambiguous existing type. + */ + coltypid = startatttuple->atttypid; + if (period->rangetypename != NULL) + { + /* Make sure it exists */ + rngtypid = TypenameGetTypidExtended(period->rangetypename, false); + if (rngtypid == InvalidOid) + ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("Range type %s not found", period->rangetypename))); + + /* Make sure it is a range type */ + if (!type_is_range(rngtypid)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("Type %s is not a range type", period->rangetypename))); + + /* Make sure it matches the column type */ + if (get_range_subtype(rngtypid) != coltypid) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("Range type %s does not match column type %s", + period->rangetypename, + format_type_be(coltypid)))); + } + else + { + rngtypid = get_subtype_range(coltypid); + if (rngtypid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("no range type for %s found for period %s", + format_type_be(coltypid), + period->periodname), + errhint("You can define a custom range type with CREATE TYPE"))); + + } + + heap_freetuple(starttuple); + heap_freetuple(endtuple); + + conoid = make_constraint_for_period(rel, period, period->constraintname, lockmode, context); + + /* Save it */ + periodoid = StorePeriod(rel, period->periodname, startattnum, endattnum, rngtypid, conoid); + + ObjectAddressSet(address, PeriodRelationId, periodoid); + + table_close(attrelation, RowExclusiveLock); + + return address; +} + +/* + * ALTER TABLE DROP PERIOD + * + * Like DROP COLUMN, we can't use the normal ALTER TABLE recursion mechanism. + */ +static void +ATExecDropPeriod(Relation rel, const char *periodName, + DropBehavior behavior, + bool recurse, bool recursing, + bool missing_ok, LOCKMODE lockmode) +{ + Relation pg_period; + Form_pg_period period; + SysScanDesc scan; + ScanKeyData key; + HeapTuple tuple; + bool found = false; + + /* At top level, permission check was done in ATPrepCmd, else do it */ + if (recursing) + ATSimplePermissions(AT_DropPeriod, rel, ATT_TABLE); + + pg_period = table_open(PeriodRelationId, RowExclusiveLock); + + /* + * Find and drop the target period + */ + ScanKeyInit(&key, + Anum_pg_period_perrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + scan = systable_beginscan(pg_period, PeriodRelidNameIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + ObjectAddress perobj; + + period = (Form_pg_period) GETSTRUCT(tuple); + + if (strcmp(NameStr(period->pername), periodName) != 0) + continue; + + /* + * Perform the actual period deletion + */ + perobj.classId = PeriodRelationId; + perobj.objectId = period->oid; + perobj.objectSubId = 0; + + performDeletion(&perobj, behavior, 0); + + found = true; + + /* period found and dropped -- no need to keep looping */ + break; + } + + systable_endscan(scan); + + if (!found) + { + if (!missing_ok) + { + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("period \"%s\" on relation \"%s\" does not exist", + periodName, RelationGetRelationName(rel)))); + } + else + { + ereport(NOTICE, + (errmsg("period \"%s\" on relation \"%s\" does not exist, skipping", + periodName, RelationGetRelationName(rel)))); + table_close(pg_period, RowExclusiveLock); + return; + } + } + + table_close(pg_period, RowExclusiveLock); +} + /* * ALTER TABLE ALTER COLUMN ADD IDENTITY * @@ -12193,6 +12670,15 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel, RememberConstraintForRebuilding(foundObject.objectId, tab); break; + case OCLASS_PERIOD: + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter type of a column used by a period"), + errdetail("%s depends on column \"%s\"", + getObjectDescription(&foundObject, false), + colName))); + break; + case OCLASS_REWRITE: /* XXX someday see if we can cope with revising views */ ereport(ERROR, @@ -14217,7 +14703,7 @@ AlterTableMoveAll(AlterTableMoveAllStmt *stmt) EventTriggerAlterTableStart((Node *) stmt); /* OID is set by AlterTableInternal */ - AlterTableInternal(lfirst_oid(l), cmds, false); + AlterTableInternal(lfirst_oid(l), cmds, false, NULL); EventTriggerAlterTableEnd(); } diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c index 4df05a0b33..22bc499f47 100644 --- a/src/backend/commands/view.c +++ b/src/backend/commands/view.c @@ -169,7 +169,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace, } /* EventTriggerAlterTableStart called by ProcessUtilitySlow */ - AlterTableInternal(viewOid, atcmds, true); + AlterTableInternal(viewOid, atcmds, true, NULL); /* Make the new view columns visible */ CommandCounterIncrement(); @@ -201,7 +201,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace, atcmds = list_make1(atcmd); /* EventTriggerAlterTableStart called by ProcessUtilitySlow */ - AlterTableInternal(viewOid, atcmds, true); + AlterTableInternal(viewOid, atcmds, true, NULL); ObjectAddressSet(address, RelationRelationId, viewOid); diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index ad1ea2ff2f..bbaee19905 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3089,6 +3089,21 @@ _copyConstraint(const Constraint *from) return newnode; } +static Period * +_copyPeriod(const Period *from) +{ + Period *newnode = makeNode(Period); + + COPY_STRING_FIELD(periodname); + COPY_STRING_FIELD(startcolname); + COPY_STRING_FIELD(endcolname); + COPY_NODE_FIELD(options); + COPY_SCALAR_FIELD(rngtypid); + COPY_LOCATION_FIELD(location); + + return newnode; +} + static DefElem * _copyDefElem(const DefElem *from) { @@ -3648,6 +3663,7 @@ _copyIndexStmt(const IndexStmt *from) COPY_STRING_FIELD(tableSpace); COPY_NODE_FIELD(indexParams); COPY_NODE_FIELD(indexIncludingParams); + COPY_NODE_FIELD(period); COPY_NODE_FIELD(options); COPY_NODE_FIELD(whereClause); COPY_NODE_FIELD(excludeOpNames); @@ -5823,6 +5839,9 @@ copyObjectImpl(const void *from) case T_Constraint: retval = _copyConstraint(from); break; + case T_Period: + retval = _copyPeriod(from); + break; case T_DefElem: retval = _copyDefElem(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index f537d3eb96..8c8e4c5ffc 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -1391,6 +1391,7 @@ _equalIndexStmt(const IndexStmt *a, const IndexStmt *b) COMPARE_STRING_FIELD(tableSpace); COMPARE_NODE_FIELD(indexParams); COMPARE_NODE_FIELD(indexIncludingParams); + COMPARE_NODE_FIELD(period); COMPARE_NODE_FIELD(options); COMPARE_NODE_FIELD(whereClause); COMPARE_NODE_FIELD(excludeOpNames); @@ -2733,6 +2734,19 @@ _equalConstraint(const Constraint *a, const Constraint *b) return true; } +static bool +_equalPeriod(const Period *a, const Period *b) +{ + COMPARE_STRING_FIELD(periodname); + COMPARE_STRING_FIELD(startcolname); + COMPARE_STRING_FIELD(endcolname); + COMPARE_NODE_FIELD(options); + COMPARE_SCALAR_FIELD(rngtypid); + COMPARE_LOCATION_FIELD(location); + + return true; +} + static bool _equalDefElem(const DefElem *a, const DefElem *b) { @@ -3828,6 +3842,9 @@ equal(const void *a, const void *b) case T_Constraint: retval = _equalConstraint(a, b); break; + case T_Period: + retval = _equalPeriod(a, b); + break; case T_DefElem: retval = _equalDefElem(a, b); break; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index e276264882..d4c4e90c29 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -1565,6 +1565,9 @@ exprLocation(const Node *expr) case T_Constraint: loc = ((const Constraint *) expr)->location; break; + case T_Period: + loc = ((const Period *) expr)->location; + break; case T_FunctionParameter: /* just use typename's location */ loc = exprLocation((Node *) ((const FunctionParameter *) expr)->argType); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 23f23f11dc..ee03b54ddc 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -2707,6 +2707,7 @@ _outCreateStmtInfo(StringInfo str, const CreateStmt *node) { WRITE_NODE_FIELD(relation); WRITE_NODE_FIELD(tableElts); + WRITE_NODE_FIELD(periods); WRITE_NODE_FIELD(inhRelations); WRITE_NODE_FIELD(partspec); WRITE_NODE_FIELD(partbound); @@ -2738,6 +2739,27 @@ _outCreateForeignTableStmt(StringInfo str, const CreateForeignTableStmt *node) WRITE_NODE_FIELD(options); } +static void +_outAlterTableStmt(StringInfo str, const AlterTableStmt *node) +{ + WRITE_NODE_TYPE("ALTERTABLESTMT"); + + WRITE_NODE_FIELD(relation); + WRITE_NODE_FIELD(cmds); +} + +static void +_outAlterTableCmd(StringInfo str, const AlterTableCmd *node) +{ + WRITE_NODE_TYPE("ALTERTABLECMD"); + + WRITE_ENUM_FIELD(subtype, AlterTableType); + WRITE_STRING_FIELD(name); + WRITE_INT_FIELD(num); + WRITE_NODE_FIELD(def); + WRITE_BOOL_FIELD(missing_ok); +} + static void _outImportForeignSchemaStmt(StringInfo str, const ImportForeignSchemaStmt *node) { @@ -2762,6 +2784,7 @@ _outIndexStmt(StringInfo str, const IndexStmt *node) WRITE_STRING_FIELD(tableSpace); WRITE_NODE_FIELD(indexParams); WRITE_NODE_FIELD(indexIncludingParams); + WRITE_NODE_FIELD(period); WRITE_NODE_FIELD(options); WRITE_NODE_FIELD(whereClause); WRITE_NODE_FIELD(excludeOpNames); @@ -3761,6 +3784,19 @@ _outConstraint(StringInfo str, const Constraint *node) } } +static void +_outPeriod(StringInfo str, const Period *node) +{ + WRITE_NODE_TYPE("PERIOD"); + + WRITE_STRING_FIELD(periodname); + WRITE_STRING_FIELD(startcolname); + WRITE_STRING_FIELD(endcolname); + WRITE_NODE_FIELD(options); + WRITE_OID_FIELD(rngtypid); + WRITE_LOCATION_FIELD(location); +} + static void _outForeignKeyCacheInfo(StringInfo str, const ForeignKeyCacheInfo *node) { @@ -4345,6 +4381,12 @@ outNode(StringInfo str, const void *obj) case T_CreateForeignTableStmt: _outCreateForeignTableStmt(str, obj); break; + case T_AlterTableStmt: + _outAlterTableStmt(str, obj); + break; + case T_AlterTableCmd: + _outAlterTableCmd(str, obj); + break; case T_ImportForeignSchemaStmt: _outImportForeignSchemaStmt(str, obj); break; @@ -4489,6 +4531,9 @@ outNode(StringInfo str, const void *obj) case T_Constraint: _outConstraint(str, obj); break; + case T_Period: + _outPeriod(str, obj); + break; case T_FuncCall: _outFuncCall(str, obj); break; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a6d0cefa6b..8514d5f5ce 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -565,7 +565,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type col_name_keyword reserved_keyword %type bare_label_keyword -%type TableConstraint TableLikeClause +%type TableConstraint TableLikeClause TablePeriod %type TableLikeOptionList TableLikeOption %type column_compression opt_column_compression %type ColQualList @@ -698,7 +698,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY + PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PERIOD PLACING PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -2408,6 +2408,24 @@ alter_table_cmd: n->def = (Node *) $4; $$ = (Node *)n; } + /* ALTER TABLE ADD PERIOD FOR (, ) */ + | ADD_P TablePeriod + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_AddPeriod; + n->def = $2; + $$ = (Node *)n; + } + /* ALTER TABLE DROP PERIOD FOR [RESTRICT|CASCADE] */ + | DROP PERIOD FOR name opt_drop_behavior + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_DropPeriod; + n->name = $4; + n->behavior = $5; + n->missing_ok = false; + $$ = (Node *)n; + } /* ALTER TABLE ADD CONSTRAINT ... */ | ADD_P TableConstraint { @@ -3469,8 +3487,10 @@ TableElement: columnDef { $$ = $1; } | TableLikeClause { $$ = $1; } | TableConstraint { $$ = $1; } + | TablePeriod { $$ = $1; } ; + TypedTableElement: columnOptions { $$ = $1; } | TableConstraint { $$ = $1; } @@ -3781,6 +3801,19 @@ TableLikeOption: ; +TablePeriod: + PERIOD FOR name '(' name ',' name ')' opt_definition + { + Period *n = makeNode(Period); + n->periodname = $3; + n->startcolname = $5; + n->endcolname = $7; + n->options = $9; + n->location = @1; + $$ = (Node *) n; + } + ; + /* ConstraintElem specifies constraint syntax which is not embedded into * a column definition. ColConstraintElem specifies the embedded form. * - thomas 1997-12-03 @@ -6560,6 +6593,14 @@ CommentStmt: n->comment = $9; $$ = (Node *) n; } + | COMMENT ON PERIOD any_name IS comment_text + { + CommentStmt *n = makeNode(CommentStmt); + n->objtype = OBJECT_PERIOD; + n->object = (Node *) $4; + n->comment = $6; + $$ = (Node *) n; + } | COMMENT ON LARGE_P OBJECT_P NumericOnly IS comment_text { CommentStmt *n = makeNode(CommentStmt); @@ -16017,6 +16058,7 @@ reserved_keyword: | ONLY | OR | ORDER + | PERIOD | PLACING | PRIMARY | REFERENCES @@ -16311,6 +16353,7 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PERIOD | PLACING | PLANS | POLICY diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c index 313d7b6ff0..ad95ba1f90 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -80,6 +80,7 @@ typedef struct bool isforeign; /* true if CREATE/ALTER FOREIGN TABLE */ bool isalter; /* true if altering existing table */ List *columns; /* ColumnDef items */ + List *periods; /* Period items */ List *ckconstraints; /* CHECK constraints */ List *fkconstraints; /* FOREIGN KEY constraints */ List *ixconstraints; /* index-creating constraints */ @@ -112,6 +113,8 @@ typedef struct static void transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column); +static void transformTablePeriod(CreateStmtContext *cxt, + Period *period); static void transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint); static void transformTableLikeClause(CreateStmtContext *cxt, @@ -231,6 +234,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) cxt.inhRelations = stmt->inhRelations; cxt.isalter = false; cxt.columns = NIL; + cxt.periods = NIL; cxt.ckconstraints = NIL; cxt.fkconstraints = NIL; cxt.ixconstraints = NIL; @@ -270,6 +274,10 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) transformColumnDefinition(&cxt, (ColumnDef *) element); break; + case T_Period: + transformTablePeriod(&cxt, (Period *) element); + break; + case T_Constraint: transformTableConstraint(&cxt, (Constraint *) element); break; @@ -337,6 +345,7 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString) * Output results. */ stmt->tableElts = cxt.columns; + stmt->periods = cxt.periods; stmt->constraints = cxt.ckconstraints; result = lappend(cxt.blist, stmt); @@ -857,6 +866,136 @@ transformColumnDefinition(CreateStmtContext *cxt, ColumnDef *column) } } +void +transformPeriodOptions(Period *period) +{ + ListCell *option; + DefElem *dconstraintname = NULL; + DefElem *drangetypename = NULL; + + foreach(option, period->options) + { + DefElem *defel = (DefElem *) lfirst(option); + + if (strcmp(defel->defname, "check_constraint_name") == 0) + { + if (dconstraintname) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + dconstraintname = defel; + } + else if (strcmp(defel->defname, "rangetype") == 0) + { + if (drangetypename) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + drangetypename = defel; + } + else + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("option \"%s\" not recognized", defel->defname))); + } + + if (dconstraintname != NULL) + period->constraintname = defGetString(dconstraintname); + else + period->constraintname = NULL; + + if (drangetypename != NULL) + period->rangetypename = defGetString(drangetypename); + else + period->rangetypename = NULL; +} + +/* + * transformTablePeriod + * transform a Period node within CREATE TABLE or ALTER TABLE + */ +static void +transformTablePeriod(CreateStmtContext *cxt, Period *period) +{ + Oid coltypid; + ColumnDef *col; + ListCell *columns; + + if (strcmp(period->periodname, "system_time") == 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("PERIOD FOR SYSTEM_TIME is not supported"), + parser_errposition(cxt->pstate, + period->location))); + + /* + * Determine the column info and range type so that transformIndexConstraints + * knows how to create PRIMARY KEY/UNIQUE constraints using this PERIOD. + */ + transformPeriodOptions(period); + + /* + * Find a suitable range type for operations involving this period. + * Use the rangetype option if provided, otherwise try to find a + * non-ambiguous existing type. + */ + coltypid = InvalidOid; + + /* First find out the type of the period's columns */ + period->rngtypid = InvalidOid; + foreach (columns, cxt->columns) + { + col = (ColumnDef *) lfirst(columns); + if (strcmp(col->colname, period->startcolname) == 0) + { + coltypid = typenameTypeId(cxt->pstate, col->typeName); + break; + } + } + if (coltypid == InvalidOid) + ereport(ERROR, (errmsg("column \"%s\" of relation \"%s\" does not exist", + period->startcolname, cxt->relation->relname))); + + /* Now make sure it matches rangetypename or we can find a matching range */ + if (period->rangetypename != NULL) + { + /* Make sure it exists */ + period->rngtypid = TypenameGetTypidExtended(period->rangetypename, false); + if (period->rngtypid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("Range type %s not found", period->rangetypename))); + + /* Make sure it is a range type */ + if (!type_is_range(period->rngtypid)) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("Type %s is not a range type", period->rangetypename))); + + /* Make sure it matches the column type */ + if (get_range_subtype(period->rngtypid) != coltypid) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("Range type %s does not match column type %s", + period->rangetypename, + format_type_be(coltypid)))); + } + else + { + period->rngtypid = get_subtype_range(coltypid); + if (period->rngtypid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("no range type for %s found for period %s", + format_type_be(coltypid), + period->periodname), + errhint("You can define a custom range type with CREATE TYPE"))); + + } + + cxt->periods = lappend(cxt->periods, period); +} + /* * transformTableConstraint * transform a Constraint node within CREATE TABLE or ALTER TABLE @@ -1587,6 +1726,11 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, index->if_not_exists = false; index->reset_default_tblspc = false; + /* Copy the period */ + Period *p = makeNode(Period); + p->oid = idxrec->indperiod; + index->period = p; + /* * We don't try to preserve the name of the source index; instead, just * let DefineIndex() choose a reasonable name. (If we tried to preserve @@ -2869,6 +3013,10 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString) } } + /* take care of the period */ + if (stmt->period) + stmt->period->oid = get_period_oid(relid, stmt->period->periodname, false); + /* * Check that only the base rel is mentioned. (This should be dead code * now that add_missing_from is history.) @@ -3330,6 +3478,7 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, cxt.inhRelations = NIL; cxt.isalter = true; cxt.columns = NIL; + cxt.periods = NIL; cxt.ckconstraints = NIL; cxt.fkconstraints = NIL; cxt.ixconstraints = NIL; @@ -3393,6 +3542,12 @@ transformAlterTableStmt(Oid relid, AlterTableStmt *stmt, (int) nodeTag(cmd->def)); break; + case AT_AddPeriod: + { + newcmds = lappend(newcmds, cmd); + break; + } + case AT_AlterColumnType: { ColumnDef *def = castNode(ColumnDef, cmd->def); diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 4ebaa552a2..73ccea4a69 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -30,6 +30,7 @@ #include "catalog/pg_namespace.h" #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" +#include "catalog/pg_period.h" #include "catalog/pg_proc.h" #include "catalog/pg_range.h" #include "catalog/pg_statistic.h" @@ -1020,6 +1021,68 @@ get_attoptions(Oid relid, int16 attnum) return result; } +/* ---------- PG_PERIOD CACHE ---------- */ + +/* + * get_periodname - given its OID, look up a period + * + * If missing_ok is false, throw an error if the period is not found. + * If true, just return InvalidOid. + */ +char * +get_periodname(Oid periodid, bool missing_ok) +{ + HeapTuple tp; + + tp = SearchSysCache1(PERIODOID, + ObjectIdGetDatum(periodid)); + if (HeapTupleIsValid(tp)) + { + Form_pg_period period_tup = (Form_pg_period) GETSTRUCT(tp); + char *result; + + result = pstrdup(NameStr(period_tup->pername)); + ReleaseSysCache(tp); + return result; + } + + if (!missing_ok) + elog(ERROR, "cache lookup failed for period %d", + periodid); + return NULL; +} + +/* + * get_period_oid - gets its relation and name, look up a period + * + * If missing_ok is false, throw an error if the cast is not found. If + * true, just return InvalidOid. + */ +Oid +get_period_oid(Oid relid, const char *periodname, bool missing_ok) +{ + HeapTuple tp; + + tp = SearchSysCache2(PERIODNAME, + ObjectIdGetDatum(relid), + PointerGetDatum(periodname)); + + if (HeapTupleIsValid(tp)) + { + Form_pg_period period_tup = (Form_pg_period) GETSTRUCT(tp); + Oid result; + + result = period_tup->oid; + ReleaseSysCache(tp); + return result; + } + + if (!missing_ok) + elog(ERROR, "cache lookup failed for period %s", + periodname); + return InvalidOid; +} + /* ---------- PG_CAST CACHE ---------- */ /* @@ -3448,6 +3511,30 @@ get_multirange_range(Oid multirangeOid) return InvalidOid; } +Oid +get_subtype_range(Oid subtypeOid) +{ + CatCList *catlist; + Oid result = InvalidOid; + + catlist = SearchSysCacheList1(RANGESUBTYPE, ObjectIdGetDatum(subtypeOid)); + + if (catlist->n_members == 1) + { + HeapTuple tuple = &catlist->members[0]->tuple; + Form_pg_range rngtup = (Form_pg_range) GETSTRUCT(tuple); + result = rngtup->rngtypid; + ReleaseCatCacheList(catlist); + } + else if (catlist->n_members > 1) + ereport(ERROR, + (errcode(ERRCODE_INDETERMINATE_DATATYPE), + errmsg("ambiguous range for type %s", + format_type_be(subtypeOid)))); + + return result; +} + /* ---------- PG_INDEX CACHE ---------- */ /* diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c index 56870b46e4..2a7817669c 100644 --- a/src/backend/utils/cache/syscache.c +++ b/src/backend/utils/cache/syscache.c @@ -48,6 +48,7 @@ #include "catalog/pg_operator.h" #include "catalog/pg_opfamily.h" #include "catalog/pg_partitioned_table.h" +#include "catalog/pg_period.h" #include "catalog/pg_proc.h" #include "catalog/pg_publication.h" #include "catalog/pg_publication_namespace.h" @@ -585,6 +586,27 @@ static const struct cachedesc cacheinfo[] = { }, 32 }, + {PeriodRelationId, /* PERIODNAME */ + PeriodRelidNameIndexId, + 2, + { + Anum_pg_period_perrelid, + Anum_pg_period_pername, + 0, + 0 + }, + 32 + }, + {PeriodRelationId, /* PERIODOID */ + PeriodObjectIndexId, + 1, + { + Anum_pg_period_oid, + 0, + 0 + }, + 32 + }, {ProcedureRelationId, /* PROCNAMEARGSNSP */ ProcedureNameArgsNspIndexId, 3, @@ -684,7 +706,17 @@ static const struct cachedesc cacheinfo[] = { }, 4 }, - + {RangeRelationId, /* RANGESUBTYPE */ + RangeSubTypidTypidIndexId, + 2, + { + Anum_pg_range_rngsubtype, + Anum_pg_range_rngtypid, + 0, + 0 + }, + 4 + }, {RangeRelationId, /* RANGETYPE */ RangeTypidIndexId, 1, diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c index 2c4cfb9457..0f3a55f1fe 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -3613,6 +3613,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData) strcmp(te->desc, "DATABASE PROPERTIES") == 0 || strcmp(te->desc, "DEFAULT") == 0 || strcmp(te->desc, "FK CONSTRAINT") == 0 || + strcmp(te->desc, "PERIOD") == 0 || strcmp(te->desc, "INDEX") == 0 || strcmp(te->desc, "RULE") == 0 || strcmp(te->desc, "TRIGGER") == 0 || diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 7e98371d25..1392ca09c8 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -6360,6 +6360,7 @@ getTables(Archive *fout, int *numTables) int i_relkind; int i_rolname; int i_relchecks; + int i_nperiod; int i_relhasindex; int i_relhasrules; int i_relpages; @@ -6424,6 +6425,14 @@ getTables(Archive *fout, int *numTables) appendPQExpBufferStr(query, "c.relhasoids, "); + /* In PG15 upwards we have PERIODs. */ + if (fout->remoteVersion >= 150000) + appendPQExpBufferStr(query, + "(SELECT count(*) FROM pg_period WHERE perrelid = c.oid) AS nperiods, "); + else + appendPQExpBufferStr(query, + "NULL AS nperiods, "); + if (fout->remoteVersion >= 80400) appendPQExpBufferStr(query, "c.relhastriggers, "); @@ -6686,6 +6695,7 @@ getTables(Archive *fout, int *numTables) i_relkind = PQfnumber(res, "relkind"); i_rolname = PQfnumber(res, "rolname"); i_relchecks = PQfnumber(res, "relchecks"); + i_nperiod = PQfnumber(res, "nperiod"); i_relhasindex = PQfnumber(res, "relhasindex"); i_relhasrules = PQfnumber(res, "relhasrules"); i_relpages = PQfnumber(res, "relpages"); @@ -6762,6 +6772,7 @@ getTables(Archive *fout, int *numTables) } tblinfo[i].reltablespace = pg_strdup(PQgetvalue(res, i, i_reltablespace)); tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0); + tblinfo[i].nperiod = atoi(PQgetvalue(res, i, i_nperiod)); tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0); tblinfo[i].relpersistence = *(PQgetvalue(res, i, i_relpersistence)); tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0); @@ -8551,6 +8562,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) PGresult *res; int ntups; bool hasdefaults; + int ndumpablechecks; /* number of CHECK constraints that do + not belong to a period */ /* Don't bother to collect info for sequences */ if (tbinfo->relkind == RELKIND_SEQUENCE) @@ -8857,10 +8870,12 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) } /* - * Get info about table CHECK constraints. This is skipped for a - * data-only dump, as it is only needed for table schemas. + * Get info about table CHECK constraints that don't belong to a PERIOD. + * This is skipped for a data-only dump, as it is only needed for table + * schemas. */ - if (tbinfo->ncheck > 0 && !dopt->dataOnly) + ndumpablechecks = tbinfo->ncheck - tbinfo->nperiod; + if (ndumpablechecks > 0 && !dopt->dataOnly) { ConstraintInfo *constrs; int numConstrs; @@ -8870,7 +8885,25 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->dobj.name); resetPQExpBuffer(q); - if (fout->remoteVersion >= 90200) + if (fout->remoteVersion >= 150000) + { + /* + * PERIODs were added in v15 and we don't dump CHECK + * constraints for them. + */ + appendPQExpBuffer(q, + "SELECT tableoid, oid, conname, " + "pg_catalog.pg_get_constraintdef(oid) AS consrc, " + "conislocal, convalidated " + "FROM pg_catalog.pg_constraint " + "WHERE conrelid = '%u'::pg_catalog.oid " + " AND contype = 'c' " + " AND NOT EXISTS (SELECT FROM pg_period " + " WHERE (perrelid, perconstraint) = (conrelid, pg_constraint.oid)) " + "ORDER BY conname", + tbinfo->dobj.catId.oid); + } + else if (fout->remoteVersion >= 90200) { /* * convalidated is new in 9.2 (actually, it is there in 9.1, @@ -8912,12 +8945,12 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); numConstrs = PQntuples(res); - if (numConstrs != tbinfo->ncheck) + if (numConstrs != ndumpablechecks) { pg_log_error(ngettext("expected %d check constraint on table \"%s\" but found %d", "expected %d check constraints on table \"%s\" but found %d", - tbinfo->ncheck), - tbinfo->ncheck, tbinfo->dobj.name, numConstrs); + ndumpablechecks), + ndumpablechecks, tbinfo->dobj.name, numConstrs); pg_log_error("(The system catalogs might be corrupted.)"); exit_nicely(1); } @@ -8975,6 +9008,76 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) } PQclear(res); } + + /* + * Get info about PERIOD definitions + */ + if (tbinfo->nperiod > 0) + { + PeriodInfo *periods; + int numPeriods; + int j; + + /* We shouldn't have any periods before v15 */ + Assert(fout->remoteVersion >= 150000); + + pg_log_info("finding periods for table \"%s.%s\"\n", + tbinfo->dobj.namespace->dobj.name, + tbinfo->dobj.name); + + resetPQExpBuffer(q); + appendPQExpBuffer(q, + "SELECT p.tableoid, p.oid, p.pername, " + " sa.attname AS perstart, ea.attname AS perend, " + " no.nspname AS opcnamespace, o.opcname, " + " c.conname AS conname " + "FROM pg_catalog.pg_period AS p " + "JOIN pg_catalog.pg_attribute AS sa ON (sa.attrelid, sa.attnum) = (p.perrelid, p.perstart) " + "JOIN pg_catalog.pg_attribute AS ea ON (ea.attrelid, ea.attnum) = (p.perrelid, p.perend) " + "JOIN pg_catalog.pg_opclass AS o ON o.oid = p.peropclass " + "JOIN pg_catalog.pg_namespace AS no ON no.oid = o.opcnamespace " + "JOIN pg_catalog.pg_constraint AS c ON c.oid = p.perconstraint " + "WHERE p.perrelid = '%u'::pg_catalog.oid " + "ORDER BY p.pername", + tbinfo->dobj.catId.oid); + + res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK); + + /* + * If we didn't get the number of rows we thought we were going to, + * then those JOINs didn't work. + */ + numPeriods = PQntuples(res); + if (numPeriods != tbinfo->nperiod) + { + pg_log_info(ngettext("expected %d period on table \"%s\" but found %d\n", + "expected %d periods on table \"%s\" but found %d\n", + tbinfo->nperiod), + tbinfo->nperiod, tbinfo->dobj.name, numPeriods); + pg_log_info("(The system catalogs might be corrupted.)\n"); + exit_nicely(1); + } + + periods = (PeriodInfo *) pg_malloc(numPeriods * sizeof(PeriodInfo)); + tbinfo->periods = periods; + + for (j = 0; j < numPeriods; j++) + { + periods[j].dobj.objType = DO_PERIOD; + periods[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, 0)); + periods[j].dobj.catId.oid = atooid(PQgetvalue(res, j, 1)); + AssignDumpId(&periods[j].dobj); + periods[j].dobj.name = pg_strdup(PQgetvalue(res, j, 2)); + periods[j].dobj.namespace = tbinfo->dobj.namespace; + periods[j].pertable = tbinfo; + periods[j].perstart = pg_strdup(PQgetvalue(res, j, 3)); + periods[j].perend = pg_strdup(PQgetvalue(res, j, 4)); + periods[j].opcnamespace = pg_strdup(PQgetvalue(res, j, 5)); + periods[j].opcname = pg_strdup(PQgetvalue(res, j, 6)); + periods[j].conname = pg_strdup(PQgetvalue(res, j, 7)); + } + PQclear(res); + } } destroyPQExpBuffer(q); @@ -10253,6 +10356,8 @@ dumpDumpableObject(Archive *fout, const DumpableObject *dobj) case DO_FK_CONSTRAINT: dumpConstraint(fout, (const ConstraintInfo *) dobj); break; + case DO_PERIOD: + break; case DO_PROCLANG: dumpProcLang(fout, (const ProcLangInfo *) dobj); break; @@ -15983,6 +16088,34 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) } } + /* + * Add non-inherited PERIOD definitions, if any. + */ + for (j = 0; j < tbinfo->nperiod; j++) + { + PeriodInfo *period = &(tbinfo->periods[j]); + + char *name = pg_strdup(fmtId(period->dobj.name)); + char *start = pg_strdup(fmtId(period->perstart)); + char *end = pg_strdup(fmtId(period->perend)); + char *opcnamespace = pg_strdup(fmtId(period->opcnamespace)); + char *opcname = pg_strdup(fmtId(period->opcname)); + char *conname = pg_strdup(fmtId(period->conname)); + + if (actual_atts == 0) + appendPQExpBufferStr(q, " (\n "); + else + appendPQExpBufferStr(q, ",\n "); + + appendPQExpBuffer(q, "PERIOD FOR %s (%s, %s) " + "WITH (operator_class = %s.%s, constraint_name = %s)", + name, start, end, + opcnamespace, opcname, + conname); + + actual_atts++; + } + /* * Add non-inherited CHECK constraints, if any. * @@ -15991,7 +16124,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) * PARTITION that we'll emit later expects the constraint to be * there. (No need to fix conislocal: ATTACH PARTITION does that) */ - for (j = 0; j < tbinfo->ncheck; j++) + for (j = 0; j < tbinfo->ncheck - tbinfo->nperiod; j++) { ConstraintInfo *constr = &(tbinfo->checkexprs[j]); @@ -16191,7 +16324,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) * For partitions, they were already dumped, and conislocal * doesn't need fixing. */ - for (k = 0; k < tbinfo->ncheck; k++) + for (k = 0; k < tbinfo->ncheck - tbinfo->nperiod; k++) { ConstraintInfo *constr = &(tbinfo->checkexprs[k]); @@ -16473,7 +16606,7 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) dumpTableSecLabel(fout, tbinfo, reltypename); /* Dump comments on inlined table constraints */ - for (j = 0; j < tbinfo->ncheck; j++) + for (j = 0; j < tbinfo->ncheck - tbinfo->nperiod; j++) { ConstraintInfo *constr = &(tbinfo->checkexprs[j]); @@ -18556,6 +18689,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, case DO_TRIGGER: case DO_EVENT_TRIGGER: case DO_DEFAULT_ACL: + case DO_PERIOD: case DO_POLICY: case DO_PUBLICATION: case DO_PUBLICATION_REL: diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index d1d8608e9c..db989ba021 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -59,6 +59,7 @@ typedef enum DO_TRIGGER, DO_CONSTRAINT, DO_FK_CONSTRAINT, /* see note for ConstraintInfo */ + DO_PERIOD, DO_PROCLANG, DO_CAST, DO_TABLE_DATA, @@ -283,12 +284,14 @@ typedef struct _tableInfo bool rowsec; /* is row security enabled? */ bool forcerowsec; /* is row security forced? */ bool hasoids; /* does it have OIDs? */ + bool hasperiods; /* does it have any periods? */ uint32 frozenxid; /* table's relfrozenxid */ uint32 minmxid; /* table's relminmxid */ Oid toast_oid; /* toast table's OID, or 0 if none */ uint32 toast_frozenxid; /* toast table's relfrozenxid, if any */ uint32 toast_minmxid; /* toast table's relminmxid */ int ncheck; /* # of CHECK expressions */ + int nperiod; /* # of PERIOD definitions */ char *reloftype; /* underlying type for typed table */ Oid foreign_server; /* foreign server oid, if applicable */ /* these two are set only if table is a sequence owned by a column: */ @@ -328,6 +331,7 @@ typedef struct _tableInfo bool *inhNotNull; /* true if NOT NULL is inherited */ struct _attrDefInfo **attrdefs; /* DEFAULT expressions */ struct _constraintInfo *checkexprs; /* CHECK constraints */ + struct _periodInfo *periods; /* PERIOD definitions */ char *partkeydef; /* partition key definition */ char *partbound; /* partition bound definition */ bool needs_override; /* has GENERATED ALWAYS AS IDENTITY */ @@ -468,6 +472,17 @@ typedef struct _constraintInfo bool separate; /* true if must dump as separate item */ } ConstraintInfo; +typedef struct _periodInfo +{ + DumpableObject dobj; + TableInfo *pertable; + char *perstart; /* the name of the start column */ + char *perend; /* the name of the end column */ + char *opcnamespace; /* the name of the operator class schema */ + char *opcname; /* the name of the operator class */ + char *conname; /* the name of the CHECK constraint */ +} PeriodInfo; + typedef struct _procLangInfo { DumpableObject dobj; diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c index 410d1790ee..3fee695379 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -75,6 +75,7 @@ enum dbObjectTypePriorities PRIO_CONSTRAINT, PRIO_INDEX, PRIO_INDEX_ATTACH, + PRIO_PERIOD, PRIO_STATSEXT, PRIO_RULE, PRIO_TRIGGER, @@ -109,6 +110,7 @@ static const int dbObjectTypePriority[] = PRIO_ATTRDEF, /* DO_ATTRDEF */ PRIO_INDEX, /* DO_INDEX */ PRIO_INDEX_ATTACH, /* DO_INDEX_ATTACH */ + PRIO_PERIOD, /* DO_PERIOD */ PRIO_STATSEXT, /* DO_STATSEXT */ PRIO_RULE, /* DO_RULE */ PRIO_TRIGGER, /* DO_TRIGGER */ @@ -1385,6 +1387,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "FK CONSTRAINT %s (ID %d OID %u)", obj->name, obj->dumpId, obj->catId.oid); return; + case DO_PERIOD: + snprintf(buf, bufsize, + "PERIOD %s (ID %d OID %u)", + obj->name, obj->dumpId, obj->catId.oid); + return; case DO_PROCLANG: snprintf(buf, bufsize, "PROCEDURAL LANGUAGE %s (ID %d OID %u)", diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index ea721d963a..c6358f91b1 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -2519,6 +2519,40 @@ describeOneTableDetails(const char *schemaname, PGresult *result = NULL; int tuples = 0; + /* print periods */ + if (pset.sversion >= 150000) + { + printfPQExpBuffer(&buf, + "SELECT quote_ident(p.pername), quote_ident(s.attname) AS startatt, quote_ident(e.attname) AS endatt\n" + "FROM pg_period AS p\n" + "JOIN pg_attribute AS s ON (s.attrelid, s.attnum) = (p.perrelid, p.perstart)\n" + "JOIN pg_attribute AS e ON (e.attrelid, e.attnum) = (p.perrelid, p.perend)\n" + "WHERE p.perrelid = '%s'\n" + "ORDER BY 1;", + oid); + result = PSQLexec(buf.data); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + { + printTableAddFooter(&cont, _("Periods:")); + for (i = 0; i < tuples; i++) + { + /* untranslated constraint name and def */ + printfPQExpBuffer(&buf, " %s (%s, %s)", + PQgetvalue(result, i, 0), + PQgetvalue(result, i, 1), + PQgetvalue(result, i, 2)); + + printTableAddFooter(&cont, buf.data); + } + } + PQclear(result); + } + /* print indexes */ if (tableinfo.hasindex) { diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index 3eca295ff4..fac04bd0e5 100644 --- a/src/include/catalog/dependency.h +++ b/src/include/catalog/dependency.h @@ -93,6 +93,7 @@ typedef enum ObjectClass OCLASS_CAST, /* pg_cast */ OCLASS_COLLATION, /* pg_collation */ OCLASS_CONSTRAINT, /* pg_constraint */ + OCLASS_PERIOD, /* pg_period */ OCLASS_CONVERSION, /* pg_conversion */ OCLASS_DEFAULT, /* pg_attrdef */ OCLASS_LANGUAGE, /* pg_language */ diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h index 6ce480b49c..76d9597328 100644 --- a/src/include/catalog/heap.h +++ b/src/include/catalog/heap.h @@ -120,6 +120,10 @@ extern Oid StoreAttrDefault(Relation rel, AttrNumber attnum, Node *expr, bool is_internal, bool add_column_mode); +extern Oid StorePeriod(Relation rel, const char *period, + AttrNumber startnum, AttrNumber endnum, + Oid rngtypid, Oid conoid); + extern Node *cookDefault(ParseState *pstate, Node *raw_default, Oid atttypid, diff --git a/src/include/catalog/pg_index.h b/src/include/catalog/pg_index.h index a5a6075d4d..22cc1ba527 100644 --- a/src/include/catalog/pg_index.h +++ b/src/include/catalog/pg_index.h @@ -43,11 +43,11 @@ CATALOG(pg_index,2610,IndexRelationId) BKI_SCHEMA_MACRO bool indisready; /* is this index ready for inserts? */ bool indislive; /* is this index alive at all? */ bool indisreplident; /* is this index the identity for replication? */ + Oid indperiod; /* the period it contains, if any */ /* variable-length fields start here, but we allow direct access to indkey */ int2vector indkey BKI_FORCE_NOT_NULL; /* column numbers of indexed cols, * or 0 */ - #ifdef CATALOG_VARLEN oidvector indcollation BKI_LOOKUP_OPT(pg_collation) BKI_FORCE_NOT_NULL; /* collation identifiers */ oidvector indclass BKI_LOOKUP(pg_opclass) BKI_FORCE_NOT_NULL; /* opclass identifiers */ diff --git a/src/include/catalog/pg_period.h b/src/include/catalog/pg_period.h new file mode 100644 index 0000000000..a7d8a62258 --- /dev/null +++ b/src/include/catalog/pg_period.h @@ -0,0 +1,55 @@ +/*------------------------------------------------------------------------- + * + * pg_period.h + * definition of the "period" system catalog (pg_period) + * + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * + * src/include/catalog/pg_period.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_PERIOD_H +#define PG_PERIOD_H + +#include "catalog/genbki.h" +#include "catalog/pg_period_d.h" + +/* ---------------- + * pg_period definition. cpp turns this into + * typedef struct FormData_pg_period + * ---------------- + */ +CATALOG(pg_period,8000,PeriodRelationId) +{ + Oid oid; /* OID of the period */ + NameData pername; /* name of period */ + Oid perrelid; /* OID of relation containing this period */ + int16 perstart; /* column for start value */ + int16 perend; /* column for end value */ + Oid perrngtype; /* OID of the range type for this period */ + Oid perconstraint; /* OID of (start < end) constraint */ + bool perislocal; /* is the period local or inherited? */ + int32 perinhcount; /* number of parents having this period */ +} FormData_pg_period; + +/* ---------------- + * Form_pg_period corresponds to a pointer to a tuple with + * the format of pg_period relation. + * ---------------- + */ +typedef FormData_pg_period *Form_pg_period; + +DECLARE_UNIQUE_INDEX_PKEY(pg_period_oid_index, 8001, PeriodObjectIndexId, on pg_period using btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_period_perrelid_pername_index, 8002, PeriodRelidNameIndexId, on pg_period using btree(perrelid oid_ops, pername name_ops)); + +extern void RemovePeriodById(Oid periodId); + +extern Oid get_relation_period_oid(Oid relid, const char *pername, bool missing_ok); + +#endif /* PG_PERIOD_H */ diff --git a/src/include/catalog/pg_range.h b/src/include/catalog/pg_range.h index cde29114ba..ec4cf36fdd 100644 --- a/src/include/catalog/pg_range.h +++ b/src/include/catalog/pg_range.h @@ -59,6 +59,7 @@ typedef FormData_pg_range *Form_pg_range; DECLARE_UNIQUE_INDEX_PKEY(pg_range_rngtypid_index, 3542, RangeTypidIndexId, on pg_range using btree(rngtypid oid_ops)); DECLARE_UNIQUE_INDEX(pg_range_rngmultitypid_index, 2228, RangeMultirangeTypidIndexId, on pg_range using btree(rngmultitypid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_range_rngsubtype_rngtypid_index, 8003, RangeSubTypidTypidIndexId, on pg_range using btree(rngsubtype oid_ops, rngtypid oid_ops)); /* * prototypes for functions in pg_range.c diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index 336549cc5f..89239205d2 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -38,7 +38,8 @@ extern LOCKMODE AlterTableGetLockLevel(List *cmds); extern void ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lockmode); -extern void AlterTableInternal(Oid relid, List *cmds, bool recurse); +extern void AlterTableInternal(Oid relid, List *cmds, bool recurse, + struct AlterTableUtilityContext *context); extern Oid AlterTableMoveAll(AlterTableMoveAllStmt *stmt); diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 7c657c1241..8cb6def639 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -457,6 +457,7 @@ typedef enum NodeTag T_IndexElem, T_StatsElem, T_Constraint, + T_Period, T_DefElem, T_RangeTblEntry, T_RangeTblFunction, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 067138e6b5..f4462c1114 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1813,6 +1813,7 @@ typedef enum ObjectType OBJECT_OPCLASS, OBJECT_OPERATOR, OBJECT_OPFAMILY, + OBJECT_PERIOD, OBJECT_POLICY, OBJECT_PROCEDURE, OBJECT_PUBLICATION, @@ -1905,6 +1906,8 @@ typedef enum AlterTableType AT_AddIndexConstraint, /* add constraint using existing index */ AT_DropConstraint, /* drop constraint */ AT_DropConstraintRecurse, /* internal to commands/tablecmds.c */ + AT_AddPeriod, /* ADD PERIOD */ + AT_DropPeriod, /* DROP PERIOD */ AT_ReAddComment, /* internal to commands/tablecmds.c */ AT_AlterColumnType, /* alter column type */ AT_AlterColumnGenericOptions, /* alter column OPTIONS (...) */ @@ -2170,9 +2173,9 @@ typedef struct VariableShowStmt /* ---------------------- * Create Table Statement * - * NOTE: in the raw gram.y output, ColumnDef and Constraint nodes are - * intermixed in tableElts, and constraints is NIL. After parse analysis, - * tableElts contains just ColumnDefs, and constraints contains just + * NOTE: in the raw gram.y output, ColumnDef, Period, and Constraint nodes are + * intermixed in tableElts; periods and constraints are NIL. After parse analysis, + * tableElts contains just ColumnDefs, periods contains just Period nodes, and constraints contains just * Constraint nodes (in fact, only CONSTR_CHECK nodes, in the present * implementation). * ---------------------- @@ -2183,6 +2186,7 @@ typedef struct CreateStmt NodeTag type; RangeVar *relation; /* relation to create */ List *tableElts; /* column definitions (list of ColumnDef) */ + List *periods; /* periods (list of Period nodes) */ List *inhRelations; /* relations to inherit from (list of * RangeVar) */ PartitionBoundSpec *partbound; /* FOR VALUES clause */ @@ -2196,6 +2200,26 @@ typedef struct CreateStmt bool if_not_exists; /* just do nothing if it already exists? */ } CreateStmt; + +/* ---------- + * Definitions for periods in CreateStmt + * ---------- + */ + +typedef struct Period +{ + NodeTag type; + Oid oid; /* period oid, once it's transformed */ + char *periodname; /* period name */ + char *startcolname; /* name of start column */ + char *endcolname; /* name of end column */ + List *options; /* options from WITH clause */ + char *constraintname; /* name of the CHECK constraint */ + char *rangetypename; /* name of the range type */ + Oid rngtypid; /* the range type to use */ + int location; /* token location, or -1 if unknown */ +} Period; + /* ---------- * Definitions for constraints in CreateStmt * @@ -2893,6 +2917,7 @@ typedef struct IndexStmt List *indexParams; /* columns to index: a list of IndexElem */ List *indexIncludingParams; /* additional columns to index: a list * of IndexElem */ + Period *period; /* The period included in the index */ List *options; /* WITH clause options: a list of DefElem */ Node *whereClause; /* qualification (partial-index predicate) */ List *excludeOpNames; /* exclusion operator names, or NIL if none */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f836acf876..43fc561075 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -312,6 +312,7 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h index 1056bf081b..8e81d170a9 100644 --- a/src/include/parser/parse_utilcmd.h +++ b/src/include/parser/parse_utilcmd.h @@ -39,5 +39,6 @@ extern IndexStmt *generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, const struct AttrMap *attmap, Oid *constraintOid); +extern void transformPeriodOptions(Period *period); #endif /* PARSE_UTILCMD_H */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index 77871aaefc..bb8c1b6742 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -96,6 +96,8 @@ extern Oid get_atttype(Oid relid, AttrNumber attnum); extern void get_atttypetypmodcoll(Oid relid, AttrNumber attnum, Oid *typid, int32 *typmod, Oid *collid); extern Datum get_attoptions(Oid relid, int16 attnum); +extern char *get_periodname(Oid periodid, bool missing_ok); +extern Oid get_period_oid(Oid relid, const char *periodname, bool missing_ok); extern Oid get_cast_oid(Oid sourcetypeid, Oid targettypeid, bool missing_ok); extern char *get_collation_name(Oid colloid); extern bool get_collation_isdeterministic(Oid colloid); @@ -194,6 +196,7 @@ extern Oid get_range_subtype(Oid rangeOid); extern Oid get_range_collation(Oid rangeOid); extern Oid get_range_multirange(Oid rangeOid); extern Oid get_multirange_range(Oid multirangeOid); +extern Oid get_subtype_range(Oid subtypeOid); extern Oid get_index_column_opclass(Oid index_oid, int attno); extern bool get_index_isreplident(Oid index_oid); extern bool get_index_isvalid(Oid index_oid); diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h index c8cfbc30f6..ba52d7a38a 100644 --- a/src/include/utils/syscache.h +++ b/src/include/utils/syscache.h @@ -73,6 +73,8 @@ enum SysCacheIdentifier OPFAMILYAMNAMENSP, OPFAMILYOID, PARTRELID, + PERIODNAME, + PERIODOID, PROCNAMEARGSNSP, PROCOID, PUBLICATIONNAME, @@ -82,6 +84,7 @@ enum SysCacheIdentifier PUBLICATIONREL, PUBLICATIONRELMAP, RANGEMULTIRANGE, + RANGESUBTYPE, RANGETYPE, RELNAMENSP, RELOID, diff --git a/src/test/regress/expected/periods.out b/src/test/regress/expected/periods.out new file mode 100644 index 0000000000..3b9eca0f04 --- /dev/null +++ b/src/test/regress/expected/periods.out @@ -0,0 +1,79 @@ +/* System periods are not implemented */ +create table pt (id integer, ds date, de date, period for system_time (ds, de)); +ERROR: PERIOD FOR SYSTEM_TIME is not supported +LINE 2: create table pt (id integer, ds date, de date, period for sy... + ^ +/* Periods must specify actual columns */ +create table pt (id integer, ds date, de date, period for p (bogus, de)); +ERROR: column "bogus" of relation "pt" does not exist +create table pt (id integer, ds date, de date, period for p (ds, bogus)); +ERROR: column "bogus" of relation "pt" does not exist +/* Data types must match exactly */ +create table pt (id integer, ds date, de timestamp, period for p (ds, de)); +ERROR: start and end columns of period must be of same type +create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de)); +ERROR: start and end columns of period must have same collation +/* Periods must have a default BTree operator class */ +create table pt (id integer, ds xml, de xml, period for p (ds, de)); +ERROR: no range type for xml found for period p +HINT: You can define a custom range type with CREATE TYPE +/* Period and column names are in the same namespace */ +create table pt (id integer, ds date, de date, period for ctid (ds, de)); +ERROR: period name "ctid" conflicts with a system column name +create table pt (id integer, ds date, de date, period for id (ds, de)); +ERROR: period name "id" conflicts with a column name +/* Now make one that works */ +create table pt (id integer, ds date, de date, period for p (ds, de)); +/* + * CREATE TABLE currently adds an ALTER TABLE to add the periods, but let's do + * some explicit testing anyway + */ +alter table pt drop period for p; +alter table pt add period for system_time (ds, de); +ERROR: PERIOD FOR SYSTEM_TIME is not supported +alter table pt add period for p (ds, de); +/* Can't drop its columns */ +alter table pt drop column ds; +ERROR: cannot drop column ds of table pt because other objects depend on it +DETAIL: period p on table pt depends on column ds of table pt +HINT: Use DROP ... CASCADE to drop the dependent objects too. +alter table pt drop column de; +ERROR: cannot drop column de of table pt because other objects depend on it +DETAIL: period p on table pt depends on column de of table pt +HINT: Use DROP ... CASCADE to drop the dependent objects too. +/* Can't change the data types */ +alter table pt alter column ds type timestamp; +ERROR: cannot alter type of a column used by a period +DETAIL: period p on table pt depends on column "ds" +alter table pt alter column ds type timestamp; +ERROR: cannot alter type of a column used by a period +DETAIL: period p on table pt depends on column "ds" +/* column/period namespace conflicts */ +alter table pt add column p integer; +ERROR: column name "p" conflicts with a period name +alter table pt rename column id to p; +ERROR: column name "p" conflicts with a period name +/* adding columns and the period at the same time */ +create table pt2 (id integer); +alter table pt2 add column ds date, add column de date, add period for p (ds, de); +drop table pt2; +/* Ambiguous range types raise an error */ +create type mydaterange as range(subtype=date); +create table pt2 (id int, ds date, de date, period for p (ds, de)); +ERROR: ambiguous range for type date +/* You can give an explicit range type */ +create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange')); +drop type mydaterange; +ERROR: cannot drop type mydaterange because other objects depend on it +DETAIL: period p on table pt2 depends on type mydaterange +HINT: Use DROP ... CASCADE to drop the dependent objects too. +drop type mydaterange cascade; +NOTICE: drop cascades to period p on table pt2 +drop table pt2; +create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange')); +/* Range type is not found */ +create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange')); +ERROR: Range type notarange not found +/* Range type is the wrong type */ +create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange')); +ERROR: Range type tstzrange does not match column type date diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index d04dc66db9..eed75441c7 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -137,6 +137,7 @@ pg_opclass|t pg_operator|t pg_opfamily|t pg_partitioned_table|t +pg_period|t pg_policy|t pg_proc|t pg_publication|t @@ -167,6 +168,8 @@ pg_type|t pg_user_mapping|t point_tbl|t polygon_tbl|t +pt|f +pt2|f quad_box_tbl|t quad_box_tbl_ord_seq1|f quad_box_tbl_ord_seq2|f diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 017e962fed..7a3b6fb145 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -58,7 +58,7 @@ test: create_index create_index_spgist create_view index_including index_includi # ---------- # Another group of parallel tests # ---------- -test: create_aggregate create_function_3 create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse +test: create_aggregate create_function_3 create_cast constraints triggers select inherit typed_table periods vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse # ---------- # sanity_check does a vacuum, affecting the sort order of SELECT * diff --git a/src/test/regress/sql/periods.sql b/src/test/regress/sql/periods.sql new file mode 100644 index 0000000000..61eaf356bc --- /dev/null +++ b/src/test/regress/sql/periods.sql @@ -0,0 +1,62 @@ +/* System periods are not implemented */ +create table pt (id integer, ds date, de date, period for system_time (ds, de)); + +/* Periods must specify actual columns */ +create table pt (id integer, ds date, de date, period for p (bogus, de)); +create table pt (id integer, ds date, de date, period for p (ds, bogus)); + +/* Data types must match exactly */ +create table pt (id integer, ds date, de timestamp, period for p (ds, de)); +create table pt (id integer, ds text collate "C", de text collate "POSIX", period for p (ds, de)); + +/* Periods must have a default BTree operator class */ +create table pt (id integer, ds xml, de xml, period for p (ds, de)); + +/* Period and column names are in the same namespace */ +create table pt (id integer, ds date, de date, period for ctid (ds, de)); +create table pt (id integer, ds date, de date, period for id (ds, de)); + +/* Now make one that works */ +create table pt (id integer, ds date, de date, period for p (ds, de)); + +/* + * CREATE TABLE currently adds an ALTER TABLE to add the periods, but let's do + * some explicit testing anyway + */ +alter table pt drop period for p; +alter table pt add period for system_time (ds, de); +alter table pt add period for p (ds, de); + +/* Can't drop its columns */ +alter table pt drop column ds; +alter table pt drop column de; + +/* Can't change the data types */ +alter table pt alter column ds type timestamp; +alter table pt alter column ds type timestamp; + +/* column/period namespace conflicts */ +alter table pt add column p integer; +alter table pt rename column id to p; + +/* adding columns and the period at the same time */ +create table pt2 (id integer); +alter table pt2 add column ds date, add column de date, add period for p (ds, de); +drop table pt2; + +/* Ambiguous range types raise an error */ +create type mydaterange as range(subtype=date); +create table pt2 (id int, ds date, de date, period for p (ds, de)); + +/* You can give an explicit range type */ +create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'mydaterange')); +drop type mydaterange; +drop type mydaterange cascade; +drop table pt2; +create table pt2 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'daterange')); + +/* Range type is not found */ +create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'notarange')); + +/* Range type is the wrong type */ +create table pt3 (id int, ds date, de date, period for p (ds, de) with (rangetype = 'tstzrange')); -- 2.25.1 --------------AA1A9756402C5229DC7A97A9 Content-Type: text/x-patch; charset=UTF-8; name="v9-0002-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename*0="v9-0002-Add-temporal-PRIMARY-KEY-and-UNIQUE-constraints.patc"; filename*1="h" From 3a47c7c6d031774bda136097fe0eaf278133cbe9 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" Date: Mon, 28 Jun 2021 17:33:27 -0700 Subject: [PATCH v9 2/4] Add temporal PRIMARY KEY and UNIQUE constraints - Added WITHOUT OVERLAPS to the bison grammar. We permit either range columns or PERIODs. - Temporal PRIMARY KEYs and UNIQUE constraints are backed by GiST indexes instead of B-tree indexes, since they are essentially exclusion constraints with = for the scalar parts of the key and && for the temporal part. With PERIODs we index a range expression using the PERIOD start and end columns. With ranges we can just index the column, so no expression is needed. - Added pg_index.indperiod to record when a PERIOD is used in an index constraint. - Added pg_constraint.contemporal to say whether a constraint is a temporal constraint. - Added pg_constraint.conperiod to record the PERIOD oid if a PERIOD is used. For range columns we can just include the column attnum, like any other column appearing in a constraint. - Added docs and tests. - Added pg_dump support. --- doc/src/sgml/ddl.sgml | 6 + doc/src/sgml/ref/create_table.sgml | 39 ++- src/backend/catalog/Catalog.pm | 1 + src/backend/catalog/heap.c | 2 + src/backend/catalog/index.c | 57 +++- src/backend/catalog/pg_constraint.c | 15 +- src/backend/catalog/toasting.c | 1 + src/backend/commands/indexcmds.c | 41 ++- src/backend/commands/tablecmds.c | 17 +- src/backend/commands/trigger.c | 2 + src/backend/commands/typecmds.c | 2 + src/backend/nodes/copyfuncs.c | 2 + src/backend/nodes/makefuncs.c | 4 +- src/backend/parser/gram.y | 32 +- src/backend/parser/parse_utilcmd.c | 228 +++++++++++++- src/backend/utils/adt/ruleutils.c | 55 +++- src/backend/utils/cache/lsyscache.c | 26 ++ src/backend/utils/cache/relcache.c | 22 +- src/bin/pg_dump/pg_dump.c | 39 ++- src/bin/pg_dump/pg_dump.h | 1 + src/bin/pg_dump/t/002_pg_dump.pl | 22 ++ src/bin/psql/describe.c | 2 + src/include/catalog/index.h | 1 + src/include/catalog/pg_constraint.h | 12 +- src/include/commands/defrem.h | 3 +- src/include/nodes/execnodes.h | 4 + src/include/nodes/makefuncs.h | 3 +- src/include/nodes/parsenodes.h | 4 + src/include/utils/lsyscache.h | 1 + src/test/regress/expected/sanity_check.out | 2 + .../regress/expected/without_overlaps.out | 286 +++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/without_overlaps.sql | 295 ++++++++++++++++++ 33 files changed, 1169 insertions(+), 60 deletions(-) create mode 100644 src/test/regress/expected/without_overlaps.out create mode 100644 src/test/regress/sql/without_overlaps.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 1b8f524191..8bc60eb843 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1182,6 +1182,12 @@ CREATE TABLE billing_addresses ( Application periods can be used to define temporal primary and foreign keys. Any table with a temporal primary key is a temporal table and supports temporal update and delete commands. + + + Temporal primary keys enforce a modified version of referential integrity called temporal referential integrity. They have two kinds of element: first one or more columns that behave as in ordinary primary keys, uniquely determiniing an entity, + and second a period (or range column) that qualifies when the row applies. So a temporal primary permits multiple rows with equal values in the ordinary key parts, as long as those rows don't have overlapping periods. Each row makes a statement about the entity identified by the ordinary key parts, but applying only to the span given by the period. + Temporal primary keys are essentially exclusion constraints where the first key parts are compared for equality and the last part for overlaps. + diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 7d3ef82f6a..fcf15013a7 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -85,8 +85,8 @@ PERIOD FOR { period_name | SYSTEM_T [ CONSTRAINT constraint_name ] { CHECK ( expression ) [ NO INHERIT ] | - UNIQUE ( column_name [, ... ] ) index_parameters | - PRIMARY KEY ( column_name [, ... ] ) index_parameters | + UNIQUE ( column_name [, ... ] [, temporal_interval WITHOUT OVERLAPS ] ) index_parameters | + PRIMARY KEY ( column_name [, ... ] [, temporal_interval WITHOUT OVERLAPS ] ) index_parameters | EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ] | FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE numeric_literal, REM { column_name | ( expression ) } [ opclass ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] +temporal_interval in a PRIMARY KEY, UNIQUE, or FOREIGN KEY constraint is: + +range_column_name | period_name + referential_action in a FOREIGN KEY/REFERENCES constraint is: { NO ACTION | RESTRICT | CASCADE | SET NULL | SET DEFAULT } + @@ -992,7 +997,10 @@ WITH ( MODULUS numeric_literal, REM Adding a unique constraint will automatically create a unique btree - index on the column or group of columns used in the constraint. + index on the column or group of columns used in the constraint. But if + the constraint includes a WITHOUT OVERLAPS clause, + it will use a GiST index and behave like a temporal PRIMARY + KEY: preventing duplicates only in overlapping time periods. @@ -1010,7 +1018,8 @@ WITH ( MODULUS numeric_literal, REM PRIMARY KEY (column constraint) - PRIMARY KEY ( column_name [, ... ] ) + PRIMARY KEY ( column_name [, ... ] + [, temporal_interval WITHOUT OVERLAPS ] ) INCLUDE ( column_name [, ...]) (table constraint) @@ -1044,8 +1053,8 @@ WITH ( MODULUS numeric_literal, REM Adding a PRIMARY KEY constraint will automatically - create a unique btree index on the column or group of columns used in the - constraint. + create a unique btree index (or GiST if temporal) on the column or group of + columns used in the constraint. @@ -1058,6 +1067,24 @@ WITH ( MODULUS numeric_literal, REM (e.g., DROP COLUMN) can cause cascaded constraint and index deletion. + + + A PRIMARY KEY with a WITHOUT OVERLAPS option + is a temporal primary key. + The WITHOUT OVERLAPS value + must be a period or range type and is used to constrain the record's applicability + to just that interval (usually a range of dates or timestamps). + The main part of the primary key may be repeated in other rows of the table, + as long as records with the same key don't overlap in the + WITHOUT OVERLAPS column. + + + + A temporal PRIMARY KEY is enforced with an + EXCLUDE constraint rather than a UNIQUE + constraint, backed by a GiST index. You may need to install the + extension to create temporal primary keys. + diff --git a/src/backend/catalog/Catalog.pm b/src/backend/catalog/Catalog.pm index 6eef0bc680..992eb4b415 100644 --- a/src/backend/catalog/Catalog.pm +++ b/src/backend/catalog/Catalog.pm @@ -266,6 +266,7 @@ sub ParseData # Scan the input file. while (<$ifd>) { + next if /^#/; my $hash_ref; if (/{/) diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 49cb627f5e..d8b99ade5c 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2558,6 +2558,8 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, is_local, /* conislocal */ inhcount, /* coninhcount */ is_no_inherit, /* connoinherit */ + false, /* contemporal */ + InvalidOid, /* conperiod */ is_internal); /* internally constructed? */ pfree(ccbin); diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index c255806e38..68c5700f6c 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -50,6 +50,7 @@ #include "catalog/pg_inherits.h" #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" +#include "catalog/pg_period.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" @@ -234,13 +235,16 @@ index_check_primary_key(Relation heapRel, HeapTuple atttuple; Form_pg_attribute attform; - if (attnum == 0) + if (attnum == 0 && !(stmt->istemporal && i > 0)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("primary keys cannot be expressions"))); - /* System attributes are never null, so no need to check */ - if (attnum < 0) + /* + * System attributes are never null, so no need to check. + * Also skip expressions. + */ + if (attnum <= 0) continue; atttuple = SearchSysCache2(ATTNUM, @@ -546,6 +550,7 @@ UpdateIndexRelation(Oid indexoid, bool isready) { int2vector *indkey; + Oid indperiod; oidvector *indcollation; oidvector *indclass; int2vector *indoption; @@ -564,6 +569,7 @@ UpdateIndexRelation(Oid indexoid, indkey = buildint2vector(NULL, indexInfo->ii_NumIndexAttrs); for (i = 0; i < indexInfo->ii_NumIndexAttrs; i++) indkey->values[i] = indexInfo->ii_IndexAttrNumbers[i]; + indperiod = indexInfo->ii_Period ? ((Period *) indexInfo->ii_Period)->oid : InvalidOid; indcollation = buildoidvector(collationOids, indexInfo->ii_NumIndexKeyAttrs); indclass = buildoidvector(classOids, indexInfo->ii_NumIndexKeyAttrs); indoption = buildint2vector(coloptions, indexInfo->ii_NumIndexKeyAttrs); @@ -623,6 +629,7 @@ UpdateIndexRelation(Oid indexoid, values[Anum_pg_index_indislive - 1] = BoolGetDatum(true); values[Anum_pg_index_indisreplident - 1] = BoolGetDatum(false); values[Anum_pg_index_indkey - 1] = PointerGetDatum(indkey); + values[Anum_pg_index_indperiod - 1] = ObjectIdGetDatum(indperiod); values[Anum_pg_index_indcollation - 1] = PointerGetDatum(indcollation); values[Anum_pg_index_indclass - 1] = PointerGetDatum(indclass); values[Anum_pg_index_indoption - 1] = PointerGetDatum(indoption); @@ -1263,6 +1270,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, Datum indclassDatum, colOptionDatum, optionDatum; + Oid periodid; oidvector *indclass; int2vector *indcoloptions; bool isnull; @@ -1298,6 +1306,9 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, Assert(!isnull); indcoloptions = (int2vector *) DatumGetPointer(colOptionDatum); + /* Get the period */ + periodid = oldInfo->ii_Period ? ((Period *) oldInfo->ii_Period)->oid : InvalidOid; + /* Fetch options of index if any */ classTuple = SearchSysCache1(RELOID, oldIndexId); if (!HeapTupleIsValid(classTuple)) @@ -1349,6 +1360,7 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, indexExprs, indexPreds, oldInfo->ii_Unique, + oldInfo->ii_Temporal, false, /* not ready for inserts */ true); @@ -1375,6 +1387,16 @@ index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, newInfo->ii_OpclassOptions[i] = get_attoptions(oldIndexId, i + 1); } + /* Set the period */ + if (periodid == InvalidOid) + newInfo->ii_Period = NULL; + else + { + Period *p = makeNode(Period); + p->oid = periodid; + newInfo->ii_Period = (Node *) p; + } + /* * Now create the new index. * @@ -1874,6 +1896,7 @@ index_concurrently_set_dead(Oid heapId, Oid indexId) * INDEX_CONSTR_CREATE_UPDATE_INDEX: update the pg_index row * INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS: remove existing dependencies * of index on table's columns + * INDEX_CONSTR_CREATE_TEMPORAL: constraint is for a temporal primary key * allow_system_table_mods: allow table to be a system catalog * is_internal: index is constructed due to internal process */ @@ -1892,16 +1915,19 @@ index_constraint_create(Relation heapRelation, ObjectAddress myself, idxaddr; Oid conOid; + Oid periodid; bool deferrable; bool initdeferred; bool mark_as_primary; bool islocal; bool noinherit; + bool is_temporal; int inhcount; deferrable = (constr_flags & INDEX_CONSTR_CREATE_DEFERRABLE) != 0; initdeferred = (constr_flags & INDEX_CONSTR_CREATE_INIT_DEFERRED) != 0; mark_as_primary = (constr_flags & INDEX_CONSTR_CREATE_MARK_AS_PRIMARY) != 0; + is_temporal = (constr_flags & INDEX_CONSTR_CREATE_TEMPORAL) != 0; /* constraint creation support doesn't work while bootstrapping */ Assert(!IsBootstrapProcessingMode()); @@ -1916,7 +1942,8 @@ index_constraint_create(Relation heapRelation, /* primary/unique constraints shouldn't have any expressions */ if (indexInfo->ii_Expressions && - constraintType != CONSTRAINT_EXCLUSION) + constraintType != CONSTRAINT_EXCLUSION && + !indexInfo->ii_Temporal) elog(ERROR, "constraints cannot have index expressions"); /* @@ -1945,6 +1972,11 @@ index_constraint_create(Relation heapRelation, noinherit = true; } + if (indexInfo->ii_Period != NULL) + periodid = ((Period *)indexInfo->ii_Period)->oid; + else + periodid = InvalidOid; + /* * Construct a pg_constraint entry. */ @@ -1976,6 +2008,8 @@ index_constraint_create(Relation heapRelation, islocal, inhcount, noinherit, + is_temporal, /* contemporal */ + periodid, /* conperiod */ is_internal); /* @@ -2408,6 +2442,7 @@ BuildIndexInfo(Relation index) RelationGetIndexExpressions(index), RelationGetIndexPredicate(index), indexStruct->indisunique, + false, indexStruct->indisready, false); @@ -2415,6 +2450,16 @@ BuildIndexInfo(Relation index) for (i = 0; i < numAtts; i++) ii->ii_IndexAttrNumbers[i] = indexStruct->indkey.values[i]; + /* set the period */ + if (indexStruct->indperiod == InvalidOid) + ii->ii_Period = NULL; + else + { + Period *p = makeNode(Period); + p->oid = indexStruct->indperiod; + ii->ii_Period = (Node *) p; + } + /* fetch exclusion constraint info if any */ if (indexStruct->indisexclusion) { @@ -2467,6 +2512,7 @@ BuildDummyIndexInfo(Relation index) RelationGetDummyIndexExpressions(index), NIL, indexStruct->indisunique, + false, indexStruct->indisready, false); @@ -2474,6 +2520,9 @@ BuildDummyIndexInfo(Relation index) for (i = 0; i < numAtts; i++) ii->ii_IndexAttrNumbers[i] = indexStruct->indkey.values[i]; + /* no need for a period */ + ii->ii_Period = NULL; + /* We ignore the exclusion constraint if any */ return ii; diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index a4e890020f..7927fbde88 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -25,6 +25,7 @@ #include "catalog/objectaccess.h" #include "catalog/pg_constraint.h" #include "catalog/pg_operator.h" +#include "catalog/pg_period.h" #include "catalog/pg_type.h" #include "commands/defrem.h" #include "commands/tablecmds.h" @@ -75,6 +76,8 @@ CreateConstraintEntry(const char *constraintName, bool conIsLocal, int conInhCount, bool conNoInherit, + bool conTemporal, + Oid period, bool is_internal) { Relation conDesc; @@ -185,6 +188,8 @@ CreateConstraintEntry(const char *constraintName, values[Anum_pg_constraint_conislocal - 1] = BoolGetDatum(conIsLocal); values[Anum_pg_constraint_coninhcount - 1] = Int32GetDatum(conInhCount); values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit); + values[Anum_pg_constraint_contemporal - 1] = BoolGetDatum(conTemporal); + values[Anum_pg_constraint_conperiod - 1] = ObjectIdGetDatum(period); if (conkeyArray) values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray); @@ -236,7 +241,7 @@ CreateConstraintEntry(const char *constraintName, { /* * Register auto dependency from constraint to owning relation, or to - * specific column(s) if any are mentioned. + * specific column(s) and period if any are mentioned. */ ObjectAddress relobject; @@ -254,6 +259,14 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSet(relobject, RelationRelationId, relId); add_exact_object_address(&relobject, addrs_auto); } + + if (OidIsValid(period)) + { + ObjectAddress periodobject; + + ObjectAddressSet(periodobject, PeriodRelationId, period); + add_exact_object_address(&periodobject, addrs_auto); + } } if (OidIsValid(domainId)) diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 0db90c2011..d6d6fcd524 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -292,6 +292,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, indexInfo->ii_NumIndexKeyAttrs = 2; indexInfo->ii_IndexAttrNumbers[0] = 1; indexInfo->ii_IndexAttrNumbers[1] = 2; + indexInfo->ii_Period = NULL; indexInfo->ii_Expressions = NIL; indexInfo->ii_ExpressionsState = NIL; indexInfo->ii_Predicate = NIL; diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index c14ca27c5e..670024762e 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -81,6 +81,9 @@ static void ComputeIndexAttrs(IndexInfo *indexInfo, const char *accessMethodName, Oid accessMethodId, bool amcanorder, bool isconstraint); +static void ComputeIndexPeriod(IndexInfo *indexInfo, + Oid relId, + const char *periodName); static char *ChooseIndexName(const char *tabname, Oid namespaceId, List *colnames, List *exclusionOpNames, bool primary, bool isconstraint); @@ -164,7 +167,8 @@ bool CheckIndexCompatible(Oid oldId, const char *accessMethodName, List *attributeList, - List *exclusionOpNames) + List *exclusionOpNames, + const char *indexPeriodName) { bool isconstraint; Oid *typeObjectId; @@ -183,6 +187,8 @@ CheckIndexCompatible(Oid oldId, int old_natts; bool isnull; bool ret = true; + Oid old_periodid; + Oid new_periodid; oidvector *old_indclass; oidvector *old_indcollation; Relation irel; @@ -226,7 +232,7 @@ CheckIndexCompatible(Oid oldId, * ii_NumIndexKeyAttrs with same value. */ indexInfo = makeIndexInfo(numberOfAttributes, numberOfAttributes, - accessMethodId, NIL, NIL, false, false, false); + accessMethodId, NIL, NIL, false, false, false, false); typeObjectId = (Oid *) palloc(numberOfAttributes * sizeof(Oid)); collationObjectId = (Oid *) palloc(numberOfAttributes * sizeof(Oid)); classObjectId = (Oid *) palloc(numberOfAttributes * sizeof(Oid)); @@ -238,6 +244,7 @@ CheckIndexCompatible(Oid oldId, accessMethodName, accessMethodId, amcanorder, isconstraint); + ComputeIndexPeriod(indexInfo, relationId, indexPeriodName); /* Get the soon-obsolete pg_index tuple. */ tuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(oldId)); @@ -257,6 +264,12 @@ CheckIndexCompatible(Oid oldId, return false; } + /* The two indexes should have the same period. */ + old_periodid = indexForm->indperiod; + new_periodid = indexInfo->ii_Period ? ((Period *) indexInfo->ii_Period)->oid : InvalidOid; + if (old_periodid != new_periodid) + return false; + /* Any change in operator class or collation breaks compatibility. */ old_natts = indexForm->indnkeyatts; Assert(old_natts == numberOfAttributes); @@ -525,6 +538,7 @@ DefineIndex(Oid relationId, Oid tablespaceId; Oid createdConstraintId = InvalidOid; List *indexColNames; + char *indexPeriodName; List *allIndexParams; Relation rel; HeapTuple tuple; @@ -771,6 +785,11 @@ DefineIndex(Oid relationId, */ indexColNames = ChooseIndexColumnNames(allIndexParams); + /* + * Choose the index period name. + */ + indexPeriodName = stmt->period ? stmt->period->periodname : NULL; + /* * Select name for index if caller didn't specify */ @@ -867,6 +886,7 @@ DefineIndex(Oid relationId, NIL, /* expressions, NIL for now */ make_ands_implicit((Expr *) stmt->whereClause), stmt->unique, + stmt->istemporal, !concurrent, concurrent); @@ -881,6 +901,8 @@ DefineIndex(Oid relationId, accessMethodName, accessMethodId, amcanorder, stmt->isconstraint); + ComputeIndexPeriod(indexInfo, relationId, indexPeriodName); + /* * Extra checks when creating a PRIMARY KEY index. */ @@ -1123,6 +1145,8 @@ DefineIndex(Oid relationId, constr_flags |= INDEX_CONSTR_CREATE_DEFERRABLE; if (stmt->initdeferred) constr_flags |= INDEX_CONSTR_CREATE_INIT_DEFERRED; + if (stmt->istemporal) + constr_flags |= INDEX_CONSTR_CREATE_TEMPORAL; indexRelationId = index_create(rel, indexRelationName, indexRelationId, parentIndexId, @@ -1994,6 +2018,19 @@ ComputeIndexAttrs(IndexInfo *indexInfo, } } +static void +ComputeIndexPeriod(IndexInfo *indexInfo, Oid relId, const char *periodName) +{ + if (periodName == NULL) + indexInfo->ii_Period = NULL; + else + { + Period *p = makeNode(Period); + p->oid = get_period_oid(relId, periodName, true); + indexInfo->ii_Period = (Node *) p; + } +} + /* * Resolve possibly-defaulted operator class specification * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 745e78cdcd..469f2042f1 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -205,6 +205,7 @@ typedef struct NewConstraint Oid refrelid; /* PK rel, if FOREIGN */ Oid refindid; /* OID of PK's index, if FOREIGN */ Oid conid; /* OID of pg_constraint entry, if FOREIGN */ + bool contemporal; /* Whether the new constraint is temporal */ Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */ ExprState *qualstate; /* Execution state for CHECK expr */ } NewConstraint; @@ -9950,6 +9951,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, conislocal, /* islocal */ coninhcount, /* inhcount */ connoinherit, /* conNoInherit */ + false, /* conTemporal */ + InvalidOid, false); /* is_internal */ ObjectAddressSet(address, ConstraintRelationId, constrOid); @@ -10218,6 +10221,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, false, 1, false, + false, /* conTemporal */ + InvalidOid, false); /* @@ -10620,6 +10625,8 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) false, /* islocal */ 1, /* inhcount */ false, /* conNoInherit */ + false, /* conTemporal */ + InvalidOid, true); /* Set up partition dependencies for the new constraint */ @@ -11151,6 +11158,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, newcon->refrelid = con->confrelid; newcon->refindid = con->conindid; newcon->conid = con->oid; + newcon->contemporal = con->contemporal; newcon->qual = (Node *) fkconstraint; /* Find or create work queue entry for this table */ @@ -11309,10 +11317,12 @@ transformColumnNameList(Oid relId, List *colList, * * Look up the names, attnums, and types of the primary key attributes * for the pkrel. Also return the index OID and index opclasses of the - * index supporting the primary key. + * index supporting the primary key. If this is a temporal primary key, + * also set the WITHOUT OVERLAPS attribute name, attnum, and atttypid. * * All parameters except pkrel are output parameters. Also, the function - * return value is the number of attributes in the primary key. + * return value is the number of attributes in the primary key, + * not including the WITHOUT OVERLAPS if any. * * Used when the column list in the REFERENCES specification is omitted. */ @@ -13563,7 +13573,8 @@ TryReuseIndex(Oid oldId, IndexStmt *stmt) if (CheckIndexCompatible(oldId, stmt->accessMethod, stmt->indexParams, - stmt->excludeOpNames)) + stmt->excludeOpNames, + stmt->period ? stmt->period->periodname : NULL)) { Relation irel = index_open(oldId, NoLock); diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index d8890d2c74..313ba9cdf6 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -836,6 +836,8 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, true, /* islocal */ 0, /* inhcount */ true, /* noinherit */ + false, /* contemporal */ + InvalidOid, /* conperiod */ isInternal); /* is_internal */ } diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 9ab4034179..ec48ac32c7 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -3552,6 +3552,8 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid, true, /* is local */ 0, /* inhcount */ false, /* connoinherit */ + false, /* contemporal */ + InvalidOid, /* conperiod */ false); /* is_internal */ if (constrAddr) ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid); diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index bbaee19905..f476009d37 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3083,6 +3083,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); @@ -3675,6 +3676,7 @@ _copyIndexStmt(const IndexStmt *from) COPY_SCALAR_FIELD(unique); COPY_SCALAR_FIELD(primary); COPY_SCALAR_FIELD(isconstraint); + COPY_SCALAR_FIELD(istemporal); COPY_SCALAR_FIELD(deferrable); COPY_SCALAR_FIELD(initdeferred); COPY_SCALAR_FIELD(transformed); diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c index 7d1a01d1ed..891e477bc6 100644 --- a/src/backend/nodes/makefuncs.c +++ b/src/backend/nodes/makefuncs.c @@ -741,7 +741,8 @@ make_ands_implicit(Expr *clause) */ IndexInfo * makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions, - List *predicates, bool unique, bool isready, bool concurrent) + List *predicates, bool unique, bool temporal, bool isready, + bool concurrent) { IndexInfo *n = makeNode(IndexInfo); @@ -750,6 +751,7 @@ makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions, Assert(n->ii_NumIndexKeyAttrs != 0); Assert(n->ii_NumIndexKeyAttrs <= n->ii_NumIndexAttrs); n->ii_Unique = unique; + n->ii_Temporal = temporal; n->ii_ReadyForInserts = isready; n->ii_Concurrent = concurrent; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 8514d5f5ce..62c545f8fc 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -498,7 +498,8 @@ 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 OptWhereClause operator_def_arg @@ -3643,6 +3644,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; @@ -3843,18 +3845,19 @@ ConstraintElem: n->initially_valid = !n->skip_validation; $$ = (Node *)n; } - | UNIQUE '(' columnList ')' opt_c_include opt_definition OptConsTableSpace + | UNIQUE '(' columnList withoutOverlapsClause ')' opt_c_include opt_definition OptConsTableSpace ConstraintAttributeSpec { Constraint *n = makeNode(Constraint); n->contype = CONSTR_UNIQUE; n->location = @1; n->keys = $3; - n->including = $5; - n->options = $6; + n->without_overlaps = $4; + n->including = $6; + n->options = $7; n->indexname = NULL; - n->indexspace = $7; - processCASbits($8, @8, "UNIQUE", + n->indexspace = $8; + processCASbits($9, @9, "UNIQUE", &n->deferrable, &n->initdeferred, NULL, NULL, yyscanner); $$ = (Node *)n; @@ -3874,18 +3877,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; @@ -3896,6 +3900,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; @@ -3959,6 +3964,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 ad95ba1f90..eb17522ca3 100644 --- a/src/backend/parser/parse_utilcmd.c +++ b/src/backend/parser/parse_utilcmd.c @@ -37,6 +37,7 @@ #include "catalog/pg_constraint.h" #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" +#include "catalog/pg_period.h" #include "catalog/pg_statistic_ext.h" #include "catalog/pg_type.h" #include "commands/comment.h" @@ -128,6 +129,8 @@ static List *get_opclass(Oid opclass, Oid actual_datatype); static void transformIndexConstraints(CreateStmtContext *cxt); static IndexStmt *transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt); +static bool findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname, + Oid *typid); static void transformExtendedStatistics(CreateStmtContext *cxt); static void transformFKConstraints(CreateStmtContext *cxt, bool skipValidation, @@ -1668,6 +1671,7 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, Oid keycoltype; Datum datum; bool isnull; + Period *period; if (constraintOid) *constraintOid = InvalidOid; @@ -1721,15 +1725,16 @@ generateClonedIndexStmt(RangeVar *heapRel, Relation source_idx, index->oldFirstRelfilenodeSubid = InvalidSubTransactionId; index->unique = idxrec->indisunique; index->primary = idxrec->indisprimary; + index->istemporal = idxrec->indisprimary && idxrec->indisexclusion; index->transformed = true; /* don't need transformIndexStmt */ index->concurrent = false; index->if_not_exists = false; index->reset_default_tblspc = false; /* Copy the period */ - Period *p = makeNode(Period); - p->oid = idxrec->indperiod; - index->period = p; + period = makeNode(Period); + period->oid = idxrec->indperiod; + index->period = period; /* * We don't try to preserve the name of the source index; instead, just @@ -2305,7 +2310,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) { @@ -2323,6 +2328,7 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) */ } index->isconstraint = true; + index->istemporal = constraint->without_overlaps != NULL; index->deferrable = constraint->deferrable; index->initdeferred = constraint->initdeferred; @@ -2416,6 +2422,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), @@ -2703,6 +2714,166 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) notnullcmd->name = pstrdup(key); notnullcmds = lappend(notnullcmds, notnullcmd); } + + 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); + char *typname; + Oid typid; + + /* + * Iterate through the table's columns + * (like just a little bit above). + * If we find one whose name is the same as without_overlaps, + * validate that it's a range type. + * + * Otherwise 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. + */ + + if (findNewOrOldColumn(cxt, without_overlaps_str, &typname, &typid)) + { + if (type_is_range(typid)) + { + AlterTableCmd *notnullcmd; + + iparam->name = pstrdup(without_overlaps_str); + iparam->expr = NULL; + + /* + * Force the column to NOT NULL since it is part of the primary key. + */ + notnullcmd = makeNode(AlterTableCmd); + + notnullcmd->subtype = AT_SetNotNull; + notnullcmd->name = pstrdup(without_overlaps_str); + notnullcmds = lappend(notnullcmds, notnullcmd); + } + else + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("column \"%s\" named in WITHOUT OVERLAPS is not a range type", + without_overlaps_str))); + } + else + { + /* Look for a PERIOD, first newly-defined */ + char *startcolname = NULL; + char *endcolname = NULL; + ListCell *periods = NULL; + foreach(periods, cxt->periods) + { + Period *period = castNode(Period, lfirst(periods)); + if (strcmp(period->periodname, without_overlaps_str) == 0) + { + startcolname = period->startcolname; + endcolname = period->endcolname; + /* The period has no oid yet, but transformIndexStmt will look it up */ + index->period = period; + index->period->oid = InvalidOid; + index->period->periodname = without_overlaps_str; + break; + } + } + + if (startcolname == NULL && cxt->rel) + { + /* Look for an already-existing PERIOD */ + // TODO: locking? releasing? + HeapTuple perTuple; + Oid relid = RelationGetRelid(cxt->rel); + perTuple = SearchSysCache2(PERIODNAME, + ObjectIdGetDatum(relid), + PointerGetDatum(without_overlaps_str)); + if (HeapTupleIsValid(perTuple)) + { + Form_pg_period per = (Form_pg_period) GETSTRUCT(perTuple); + startcolname = get_attname(relid, per->perstart, false); + endcolname = get_attname(relid, per->perend, false); + index->period = makeNode(Period); + index->period->oid = per->oid; + index->period->periodname = without_overlaps_str; + + ReleaseSysCache(perTuple); + } + } + if (startcolname != NULL) + { + ColumnRef *start, *end; + Oid rngtypid; + char *range_type_name; + + if (!findNewOrOldColumn(cxt, startcolname, &typname, &typid)) + elog(ERROR, "Missing startcol %s for period %s", + startcolname, without_overlaps_str); + if (!findNewOrOldColumn(cxt, endcolname, &typname, &typid)) + elog(ERROR, "Missing endcol %s for period %s", + endcolname, without_overlaps_str); + + /* Use the start/end columns */ + + start = makeNode(ColumnRef); + start->fields = list_make1(makeString(startcolname)); + start->location = constraint->location; + + end = makeNode(ColumnRef); + end->fields = list_make1(makeString(endcolname)); + end->location = constraint->location; + + rngtypid = get_subtype_range(typid); + if (rngtypid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("PERIOD \"%s\" cannot be used in a constraint without a corresponding range type", + without_overlaps_str))); + + range_type_name = get_typname(rngtypid); + + /* Build a range to represent the PERIOD. */ + iparam->name = NULL; + iparam->expr = (Node *) makeFuncCall(SystemFuncName(range_type_name), + list_make2(start, end), + COERCE_EXPLICIT_CALL, + -1); + } + 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); + + index->excludeOpNames = lappend(index->excludeOpNames, + list_make1(makeString("&&"))); + index->accessMethod = "gist"; + constraint->access_method = "gist"; + } } } @@ -2823,6 +2994,55 @@ transformIndexConstraint(Constraint *constraint, CreateStmtContext *cxt) return index; } +/* + * Tries to find a column by name among the existing ones (if it's an ALTER TABLE) + * and the new ones. Sets typname and typid if one is found. Returns false if we + * couldn't find a match. + */ +static bool +findNewOrOldColumn(CreateStmtContext *cxt, char *colname, char **typname, Oid *typid) +{ + /* Check the new columns first in case their type is changing. */ + + ColumnDef *column = NULL; + ListCell *columns; + + foreach(columns, cxt->columns) + { + column = lfirst_node(ColumnDef, columns); + if (strcmp(column->colname, colname) == 0) + { + *typid = typenameTypeId(NULL, column->typeName); + *typname = TypeNameToString(column->typeName); + return true; + } + } + + // TODO: should I consider DROP COLUMN? + + /* Look up columns on existing table. */ + + if (cxt->isalter) + { + Relation rel = cxt->rel; + for (int i = 0; i < rel->rd_att->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i); + const char *attname = NameStr(attr->attname); + if (strcmp(attname, colname) == 0) + { + Type type = typeidType(attr->atttypid); + *typid = attr->atttypid; + *typname = pstrdup(typeTypeName(type)); + ReleaseSysCache(type); + return true; + } + } + } + + return false; +} + /* * transformExtendedStatistics * Handle extended statistic objects diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 1bb25738a5..a768e4f55f 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -332,8 +332,8 @@ static char *deparse_expression_pretty(Node *expr, List *dpcontext, static char *pg_get_viewdef_worker(Oid viewoid, int prettyFlags, int wrapColumn); static char *pg_get_triggerdef_worker(Oid trigid, bool pretty); -static int decompile_column_index_array(Datum column_index_array, Oid relId, - StringInfo buf); +static int decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId, + bool withoutOverlaps, Oid periodid, 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, @@ -2196,7 +2196,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, conForm->conindid, false, InvalidOid, &buf); /* add foreign relation name */ appendStringInfo(&buf, ") REFERENCES %s(", @@ -2210,7 +2210,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, conForm->conindid, false, InvalidOid, &buf); appendStringInfoChar(&buf, ')'); @@ -2311,12 +2311,17 @@ 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. + */ + indexId = conForm->conindid; + SysCacheGetAttr(CONSTROID, tup, + Anum_pg_constraint_conexclop, &isnull); + keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, InvalidOid, &buf); appendStringInfoChar(&buf, ')'); - indexId = conForm->conindid; - /* Build including column list (from pg_index.indkeys) */ indtup = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexId)); if (!HeapTupleIsValid(indtup)) @@ -2512,8 +2517,8 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, * of keys. */ static int -decompile_column_index_array(Datum column_index_array, Oid relId, - StringInfo buf) +decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId, + bool withoutOverlaps, Oid periodid, StringInfo buf) { Datum *keys; int nKeys; @@ -2527,11 +2532,41 @@ decompile_column_index_array(Datum column_index_array, Oid relId, for (j = 0; j < nKeys; j++) { char *colName; + int colid = DatumGetInt16(keys[j]); + + /* The key might contain a PERIOD instead of an attribute */ + if (colid == 0) + { + /* First try the given periodid, then fall back on the index */ + if (periodid == InvalidOid) + { + HeapTuple indtup; + bool isnull; + Datum periodidDatum; + + indtup = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexId)); + if (!HeapTupleIsValid(indtup)) + elog(ERROR, "cache lookup failed for index %u", indexId); - colName = get_attname(relId, DatumGetInt16(keys[j]), false); + periodidDatum = SysCacheGetAttr(INDEXRELID, indtup, + Anum_pg_index_indperiod, &isnull); + if (isnull) + elog(ERROR, "missing period for index %u", indexId); + + periodid = DatumGetObjectId(periodidDatum); + ReleaseSysCache(indtup); + } + colName = get_periodname(periodid, false); + } + else + { + colName = get_attname(relId, colid, false); + } if (j == 0) appendStringInfoString(buf, quote_identifier(colName)); + else if (withoutOverlaps && j == nKeys - 1) + appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName)); else appendStringInfo(buf, ", %s", quote_identifier(colName)); } diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 73ccea4a69..362092368f 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -2198,6 +2198,32 @@ get_typisdefined(Oid typid) return false; } +/* + * get_typname + * + * Returns the name of a given type + * + * Returns a palloc'd copy of the string, or NULL if no such type. + */ +char * +get_typname(Oid typid) +{ + HeapTuple tp; + + tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid)); + if (HeapTupleIsValid(tp)) + { + Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp); + char *result; + + result = pstrdup(NameStr(typtup->typname)); + ReleaseSysCache(tp); + return result; + } + else + return NULL; +} + /* * get_typlen * diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 9fa9e671a1..aab41779db 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4729,11 +4729,17 @@ RelationGetIndexList(Relation relation) * interesting for either oid indexes or replication identity indexes, * so don't check them. */ - if (!index->indisvalid || !index->indisunique || - !index->indimmediate || + if (!index->indisvalid || !index->indimmediate || !heap_attisnull(htup, Anum_pg_index_indpred, NULL)) continue; + /* + * Non-unique indexes aren't interesting either, + * except when they are temporal primary keys. + */ + if (!index->indisunique && !index->indisprimary) + continue; + /* remember primary key index if any */ if (index->indisprimary) pkeyIndex = index->indexrelid; @@ -5415,8 +5421,9 @@ RelationGetIdentityKeyBitmap(Relation relation) * RelationGetExclusionInfo -- get info about index's exclusion constraint * * This should be called only for an index that is known to have an - * associated exclusion constraint. It returns arrays (palloc'd in caller's - * context) of the exclusion operator OIDs, their underlying functions' + * associated exclusion constraint or temporal primary key. + * 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. */ @@ -5482,7 +5489,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 1392ca09c8..9816cc326d 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -7026,7 +7026,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++) @@ -7087,7 +7088,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) " @@ -7126,7 +7128,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 " @@ -7161,7 +7164,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 " @@ -7192,7 +7196,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 " @@ -7226,7 +7231,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 " @@ -7266,6 +7272,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)); @@ -7331,6 +7338,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables) constrinfo->condeferred = *(PQgetvalue(res, j, i_condeferred)) == 't'; constrinfo->conislocal = true; constrinfo->separate = true; + constrinfo->withoutoverlaps = *(PQgetvalue(res, j, i_withoutoverlaps)) == 't'; indxinfo[j].indexconstraint = constrinfo->dobj.dumpId; } @@ -17091,9 +17099,22 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo) break; attname = getAttrName(indkey, tbinfo); - appendPQExpBuffer(q, "%s%s", - (k == 0) ? "" : ", ", - fmtId(attname)); + if (k == 0) + { + appendPQExpBuffer(q, "%s", + fmtId(attname)); + } + else if (k == indxinfo->indnkeyattrs - 1 && + coninfo->withoutoverlaps) + { + appendPQExpBuffer(q, ", %s WITHOUT OVERLAPS", + fmtId(attname)); + } + else + { + appendPQExpBuffer(q, ", %s", + fmtId(attname)); + } } if (indxinfo->indnkeyattrs < indxinfo->indnattrs) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index db989ba021..630be50784 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -470,6 +470,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 _periodInfo diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index d293f52b05..d8caea7bd4 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -699,6 +699,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 c6358f91b1..825128ccf7 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -2624,6 +2624,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/index.h b/src/include/catalog/index.h index 008f723e10..8bc2b0be91 100644 --- a/src/include/catalog/index.h +++ b/src/include/catalog/index.h @@ -91,6 +91,7 @@ extern Oid index_create(Relation heapRelation, #define INDEX_CONSTR_CREATE_INIT_DEFERRED (1 << 2) #define INDEX_CONSTR_CREATE_UPDATE_INDEX (1 << 3) #define INDEX_CONSTR_CREATE_REMOVE_OLD_DEPS (1 << 4) +#define INDEX_CONSTR_CREATE_TEMPORAL (1 << 5) extern Oid index_concurrently_create_copy(Relation heapRelation, Oid oldIndexId, diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index e75baa8e1e..6c6675b0f7 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -107,6 +107,14 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) /* Has a local definition and cannot be inherited */ bool connoinherit; + /* + * For primary and foreign keys, signifies the last column is a range + * and should use overlaps instead of equals. + */ + bool contemporal; + + Oid conperiod; /* local PERIOD used in PK/FK constraint */ + #ifdef CATALOG_VARLEN /* variable-length fields start here */ /* @@ -140,7 +148,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) /* * If an exclusion constraint, the OIDs of the exclusion operators for - * each column of the constraint + * each column of the constraint. Also set for temporal primary keys. */ Oid conexclop[1] BKI_LOOKUP(pg_operator); @@ -227,6 +235,8 @@ extern Oid CreateConstraintEntry(const char *constraintName, bool conIsLocal, int conInhCount, bool conNoInherit, + bool conTemporal, + Oid period, bool is_internal); extern void RemoveConstraintById(Oid conId); diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index f84d09959c..b6e2d16968 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -43,7 +43,8 @@ extern char *ChooseRelationName(const char *name1, const char *name2, extern bool CheckIndexCompatible(Oid oldId, const char *accessMethodName, List *attributeList, - List *exclusionOpNames); + List *exclusionOpNames, + const char *indexPeriodName); extern Oid GetDefaultOpClass(Oid type_id, Oid am_id); extern Oid ResolveOpClass(List *opclass, Oid attrType, const char *accessMethodName, Oid accessMethodId); diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 2e8cbee69f..62cb6f5054 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -129,6 +129,7 @@ typedef struct ExprState * IndexAttrNumbers underlying-rel attribute numbers used as keys * (zeroes indicate expressions). It also contains * info about included columns. + * Period period used in the index, or NULL if none * Expressions expr trees for expression entries, or NIL if none * ExpressionsState exec state for expressions, or NIL if none * Predicate partial-index predicate, or NIL if none @@ -140,6 +141,7 @@ typedef struct ExprState * UniqueProcs * UniqueStrats * Unique is it a unique index? + * Temporal is it for a temporal constraint? * OpclassOptions opclass-specific options, or NULL if none * ReadyForInserts is it valid for inserts? * Concurrent are we doing a concurrent index build? @@ -159,6 +161,7 @@ typedef struct IndexInfo int ii_NumIndexAttrs; /* total number of columns in index */ int ii_NumIndexKeyAttrs; /* number of key columns in index */ AttrNumber ii_IndexAttrNumbers[INDEX_MAX_KEYS]; + Node *ii_Period; /* period used in the index */ List *ii_Expressions; /* list of Expr */ List *ii_ExpressionsState; /* list of ExprState */ List *ii_Predicate; /* list of Expr */ @@ -171,6 +174,7 @@ typedef struct IndexInfo uint16 *ii_UniqueStrats; /* array with one entry per column */ Datum *ii_OpclassOptions; /* array with one entry per column */ bool ii_Unique; + bool ii_Temporal; bool ii_ReadyForInserts; bool ii_Concurrent; bool ii_BrokenHotChain; diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h index eea87f847d..2ad1a48d94 100644 --- a/src/include/nodes/makefuncs.h +++ b/src/include/nodes/makefuncs.h @@ -96,7 +96,8 @@ extern List *make_ands_implicit(Expr *clause); extern IndexInfo *makeIndexInfo(int numattrs, int numkeyattrs, Oid amoid, List *expressions, List *predicates, - bool unique, bool isready, bool concurrent); + bool unique, bool temporal, bool isready, + bool concurrent); extern DefElem *makeDefElem(char *name, Node *arg, int location); extern DefElem *makeDefElemExtended(char *nameSpace, char *name, Node *arg, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index f4462c1114..647dfdc144 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2329,6 +2329,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? */ @@ -2930,6 +2933,7 @@ typedef struct IndexStmt bool unique; /* is index unique? */ bool primary; /* is index a primary key? */ bool isconstraint; /* is it for a pkey/unique constraint? */ + bool istemporal; /* is it for a temporal pkey? */ bool deferrable; /* is the constraint DEFERRABLE? */ bool initdeferred; /* is the constraint INITIALLY DEFERRED? */ bool transformed; /* true when transformIndexStmt is finished */ diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h index bb8c1b6742..b3aad218f0 100644 --- a/src/include/utils/lsyscache.h +++ b/src/include/utils/lsyscache.h @@ -144,6 +144,7 @@ extern char get_rel_persistence(Oid relid); extern Oid get_transform_fromsql(Oid typid, Oid langid, List *trftypes); extern Oid get_transform_tosql(Oid typid, Oid langid, List *trftypes); extern bool get_typisdefined(Oid typid); +extern char *get_typname(Oid typid); extern int16 get_typlen(Oid typid); extern bool get_typbyval(Oid typid); extern void get_typlenbyval(Oid typid, int16 *typlen, bool *typbyval); diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index eed75441c7..6836f41f09 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -222,6 +222,8 @@ trigger_parted_p2|t trigger_parted_p2_2|t varchar_tbl|f view_base_table|t +without_overlaps_test|t +without_overlaps_uq_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..708b5d3528 --- /dev/null +++ b/src/test/regress/expected/without_overlaps.out @@ -0,0 +1,286 @@ +-- 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/PERIOD 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 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: +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from date, + valid_til date, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'without_overlaps2_pk'; + pg_get_constraintdef +--------------------------------------------- + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +(1 row) + +DROP TABLE without_overlaps_test2; +-- PK with two columns plus a PERIOD: +CREATE TABLE without_overlaps_test2 ( + id1 int4range, + id2 int4range, + valid_from date, + valid_til date, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_test2; +-- 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; +-- UNIQUE with no columns just WITHOUT OVERLAPS: +CREATE TABLE without_overlaps_uq_test ( + valid_at tsrange, + CONSTRAINT without_overlaps_uq UNIQUE (valid_at WITHOUT OVERLAPS) +); +ERROR: syntax error at or near "WITHOUT" +LINE 3: CONSTRAINT without_overlaps_uq UNIQUE (valid_at WITHOUT OVE... + ^ +-- UNIQUE with a range column/PERIOD that isn't there: +CREATE TABLE without_overlaps_uq_test ( + id INTEGER, + CONSTRAINT without_overlaps_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +ERROR: range or PERIOD "valid_at" named in WITHOUT OVERLAPS does not exist +-- UNIQUE with a non-range column: +CREATE TABLE without_overlaps_uq_test ( + id INTEGER, + valid_at TEXT, + CONSTRAINT without_overlaps_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +ERROR: column "valid_at" named in WITHOUT OVERLAPS is not a range type +-- UNIQUE with one column plus a range: +CREATE TABLE without_overlaps_uq_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_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +-- UNIQUE with two columns plus a range: +CREATE TABLE without_overlaps_uq_test2 ( + id1 int4range, + id2 int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_uq_test2; +-- UNIQUE with one column plus a PERIOD: +CREATE TABLE without_overlaps_uq_test2 ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_uq_test2; +-- UNIQUE with two columns plus a PERIOD: +CREATE TABLE without_overlaps_uq_test2 ( + id1 int4range, + id2 int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_uq_test2; +-- UNIQUE with a custom range type: +CREATE TYPE textrange2 AS range (subtype=text, collation="C"); +CREATE TABLE without_overlaps_uq_test2 ( + id int4range, + valid_at textrange2, + CONSTRAINT without_overlaps2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE without_overlaps_uq_test2 DROP CONSTRAINT without_overlaps2_uq; +DROP TABLE without_overlaps_uq_test2; +DROP TYPE textrange2; +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +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); +-- PK with USING INDEX (not yet allowed): +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at tsrange +); +CREATE INDEX idx_without_overlaps2 ON without_overlaps_test2 USING gist (id, valid_at); +ALTER TABLE without_overlaps_test2 + ADD CONSTRAINT without_overlaps2_pk + PRIMARY KEY USING INDEX idx_without_overlaps2; +ERROR: "idx_without_overlaps2" is not a unique index +LINE 2: ADD CONSTRAINT without_overlaps2_pk + ^ +DETAIL: Cannot create a primary key or unique constraint using such an index. +DROP TABLE without_overlaps_test2; +-- UNIQUE with USING INDEX (not yet allowed): +CREATE TABLE without_overlaps_uq_test2 ( + id int4range, + valid_at tsrange +); +CREATE INDEX idx_without_overlaps_uq ON without_overlaps_uq_test2 USING gist (id, valid_at); +ALTER TABLE without_overlaps_uq_test2 + ADD CONSTRAINT without_overlaps2_uq + UNIQUE USING INDEX idx_without_overlaps_uq; +ERROR: "idx_without_overlaps_uq" is not a unique index +LINE 2: ADD CONSTRAINT without_overlaps2_uq + ^ +DETAIL: Cannot create a primary key or unique constraint using such an index. +DROP TABLE without_overlaps_uq_test2; +-- Add range column and the PK at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range +); +ALTER TABLE without_overlaps_test2 + ADD COLUMN valid_at tsrange, + ADD CONSTRAINT without_overlaps2_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; +-- Add PERIOD and the PK at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from date, + valid_til date +); +ALTER TABLE without_overlaps_test2 + ADD PERIOD FOR valid_at (valid_from, valid_til), + ADD CONSTRAINT without_overlaps2_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; +-- Add range column and UNIQUE constraint at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range +); +ALTER TABLE without_overlaps_test2 + ADD COLUMN valid_at tsrange, + ADD CONSTRAINT without_overlaps2_uq + UNIQUE (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; +-- Add PERIOD column and UNIQUE constraint at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from date, + valid_til date +); +ALTER TABLE without_overlaps_test2 + ADD PERIOD FOR valid_at (valid_from, valid_til), + ADD CONSTRAINT without_overlaps2_uq + UNIQUE (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; +-- Add date columns, PERIOD, and the PK at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range +); +ALTER TABLE without_overlaps_test2 + ADD COLUMN valid_from date, + ADD COLUMN valid_til date, + ADD PERIOD FOR valid_at (valid_from, valid_til), + ADD CONSTRAINT without_overlaps2_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; +-- Add date columns, PERIOD, and UNIQUE constraint at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range +); +ALTER TABLE without_overlaps_test2 + ADD COLUMN valid_from date, + ADD COLUMN valid_til date, + ADD PERIOD FOR valid_at (valid_from, valid_til), + ADD CONSTRAINT without_overlaps2_uq + UNIQUE (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; +-- +-- 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" of relation "without_overlaps_test" 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" of relation "without_overlaps_test" 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 7a3b6fb145..088387281a 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -30,7 +30,7 @@ test: strings numerology point lseg line box path polygon circle date time timet # horology depends on interval, timetz, timestamp, timestamptz # opr_sanity depends on create_function_0 # ---------- -test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc +test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc without_overlaps # ---------- # These four each depend on the previous one diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql new file mode 100644 index 0000000000..e64ec69f0b --- /dev/null +++ b/src/test/regress/sql/without_overlaps.sql @@ -0,0 +1,295 @@ +-- 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/PERIOD 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 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: +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from date, + valid_til date, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'without_overlaps2_pk'; +DROP TABLE without_overlaps_test2; + +-- PK with two columns plus a PERIOD: +CREATE TABLE without_overlaps_test2 ( + id1 int4range, + id2 int4range, + valid_from date, + valid_til date, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id1, id2, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_test2; + +-- 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; + +-- UNIQUE with no columns just WITHOUT OVERLAPS: + +CREATE TABLE without_overlaps_uq_test ( + valid_at tsrange, + CONSTRAINT without_overlaps_uq UNIQUE (valid_at WITHOUT OVERLAPS) +); + +-- UNIQUE with a range column/PERIOD that isn't there: + +CREATE TABLE without_overlaps_uq_test ( + id INTEGER, + CONSTRAINT without_overlaps_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); + +-- UNIQUE with a non-range column: + +CREATE TABLE without_overlaps_uq_test ( + id INTEGER, + valid_at TEXT, + CONSTRAINT without_overlaps_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); + +-- UNIQUE with one column plus a range: + +CREATE TABLE without_overlaps_uq_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_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); + +-- UNIQUE with two columns plus a range: +CREATE TABLE without_overlaps_uq_test2 ( + id1 int4range, + id2 int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_uq_test2; + +-- UNIQUE with one column plus a PERIOD: +CREATE TABLE without_overlaps_uq_test2 ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_uq_test2; + +-- UNIQUE with two columns plus a PERIOD: +CREATE TABLE without_overlaps_uq_test2 ( + id1 int4range, + id2 int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_uq UNIQUE (id1, id2, valid_at WITHOUT OVERLAPS) +); +DROP TABLE without_overlaps_uq_test2; + +-- UNIQUE with a custom range type: +CREATE TYPE textrange2 AS range (subtype=text, collation="C"); +CREATE TABLE without_overlaps_uq_test2 ( + id int4range, + valid_at textrange2, + CONSTRAINT without_overlaps2_uq UNIQUE (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE without_overlaps_uq_test2 DROP CONSTRAINT without_overlaps2_uq; +DROP TABLE without_overlaps_uq_test2; +DROP TYPE textrange2; + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +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); + +-- PK with USING INDEX (not yet allowed): +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at tsrange +); +CREATE INDEX idx_without_overlaps2 ON without_overlaps_test2 USING gist (id, valid_at); +ALTER TABLE without_overlaps_test2 + ADD CONSTRAINT without_overlaps2_pk + PRIMARY KEY USING INDEX idx_without_overlaps2; +DROP TABLE without_overlaps_test2; + +-- UNIQUE with USING INDEX (not yet allowed): +CREATE TABLE without_overlaps_uq_test2 ( + id int4range, + valid_at tsrange +); +CREATE INDEX idx_without_overlaps_uq ON without_overlaps_uq_test2 USING gist (id, valid_at); +ALTER TABLE without_overlaps_uq_test2 + ADD CONSTRAINT without_overlaps2_uq + UNIQUE USING INDEX idx_without_overlaps_uq; +DROP TABLE without_overlaps_uq_test2; + +-- Add range column and the PK at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range +); +ALTER TABLE without_overlaps_test2 + ADD COLUMN valid_at tsrange, + ADD CONSTRAINT without_overlaps2_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; + +-- Add PERIOD and the PK at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from date, + valid_til date +); +ALTER TABLE without_overlaps_test2 + ADD PERIOD FOR valid_at (valid_from, valid_til), + ADD CONSTRAINT without_overlaps2_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; + +-- Add range column and UNIQUE constraint at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range +); +ALTER TABLE without_overlaps_test2 + ADD COLUMN valid_at tsrange, + ADD CONSTRAINT without_overlaps2_uq + UNIQUE (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; + +-- Add PERIOD column and UNIQUE constraint at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from date, + valid_til date +); +ALTER TABLE without_overlaps_test2 + ADD PERIOD FOR valid_at (valid_from, valid_til), + ADD CONSTRAINT without_overlaps2_uq + UNIQUE (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; + +-- Add date columns, PERIOD, and the PK at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range +); +ALTER TABLE without_overlaps_test2 + ADD COLUMN valid_from date, + ADD COLUMN valid_til date, + ADD PERIOD FOR valid_at (valid_from, valid_til), + ADD CONSTRAINT without_overlaps2_pk + PRIMARY KEY (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; + +-- Add date columns, PERIOD, and UNIQUE constraint at the same time +CREATE TABLE without_overlaps_test2 ( + id int4range +); +ALTER TABLE without_overlaps_test2 + ADD COLUMN valid_from date, + ADD COLUMN valid_til date, + ADD PERIOD FOR valid_at (valid_from, valid_til), + ADD CONSTRAINT without_overlaps2_uq + UNIQUE (id, valid_at WITHOUT OVERLAPS); +DROP TABLE without_overlaps_test2; + +-- +-- 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; -- 2.25.1 --------------AA1A9756402C5229DC7A97A9 Content-Type: text/x-patch; charset=UTF-8; name="v9-0003-Add-UPDATE-DELETE-FOR-PORTION-OF.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="v9-0003-Add-UPDATE-DELETE-FOR-PORTION-OF.patch" From 65131972fe5b3cd02c2b740a6c14509a551c3b15 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" Date: Fri, 25 Jun 2021 18:54:35 -0700 Subject: [PATCH v9 3/4] Add UPDATE/DELETE FOR PORTION OF - Added bison support for FOR PORTION OF syntax. The bounds must be constant, so we forbid column references, subqueries, etc. But we permit the magic MINVALUE and MAXVALUE keywords (to match the terms for partitions) to perform an unbounded update/delete. We also accept functions like NOW(). - Added AFTER ROW triggers to insert new rows for the "leftover" part of a record touched by a FOR PORTION OF query. We install these triggers on any table with a temporal primary key. - Added tg_temporal descriptor to the TriggerData struct that we pass to trigger functions. Our triggers use this to learn what bounds were given in the FOR PORTION OF clause. - Documented FOR PORTION OF. - Documented tg_temporal struct. - Added tests. --- doc/src/sgml/ddl.sgml | 4 + doc/src/sgml/ref/delete.sgml | 46 ++ doc/src/sgml/ref/update.sgml | 47 ++ doc/src/sgml/trigger.sgml | 60 +- src/backend/access/brin/brin_minmax_multi.c | 28 +- src/backend/commands/indexcmds.c | 6 + src/backend/commands/tablecmds.c | 65 ++ src/backend/commands/trigger.c | 55 ++ src/backend/executor/execMain.c | 1 + src/backend/executor/nodeModifyTable.c | 33 + src/backend/nodes/copyfuncs.c | 47 ++ src/backend/nodes/equalfuncs.c | 35 + src/backend/nodes/nodeFuncs.c | 10 + src/backend/nodes/outfuncs.c | 23 + src/backend/nodes/readfuncs.c | 25 + src/backend/optimizer/plan/createplan.c | 8 +- src/backend/optimizer/plan/planner.c | 1 + src/backend/optimizer/util/pathnode.c | 3 +- src/backend/parser/analyze.c | 384 ++++++++++- src/backend/parser/gram.y | 47 +- src/backend/parser/parse_agg.c | 10 + src/backend/parser/parse_collate.c | 1 + src/backend/parser/parse_expr.c | 8 + src/backend/parser/parse_func.c | 3 + src/backend/rewrite/rewriteHandler.c | 24 + src/backend/tcop/utility.c | 2 +- src/backend/utils/adt/Makefile | 1 + src/backend/utils/adt/fp_triggers.c | 574 ++++++++++++++++ src/backend/utils/adt/rangetypes.c | 42 ++ src/include/catalog/pg_proc.dat | 6 + src/include/commands/tablecmds.h | 2 + src/include/commands/trigger.h | 1 + src/include/nodes/execnodes.h | 22 + src/include/nodes/nodes.h | 3 + src/include/nodes/parsenodes.h | 43 +- src/include/nodes/pathnodes.h | 1 + src/include/nodes/plannodes.h | 2 + src/include/nodes/primnodes.h | 27 + src/include/optimizer/pathnode.h | 2 +- src/include/parser/kwlist.h | 1 + src/include/parser/parse_node.h | 1 + src/include/utils/rangetypes.h | 3 + src/test/regress/expected/for_portion_of.out | 674 +++++++++++++++++++ src/test/regress/expected/sanity_check.out | 1 + src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/for_portion_of.sql | 562 ++++++++++++++++ 46 files changed, 2888 insertions(+), 58 deletions(-) create mode 100644 src/backend/utils/adt/fp_triggers.c create mode 100644 src/test/regress/expected/for_portion_of.out create mode 100644 src/test/regress/sql/for_portion_of.sql diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 8bc60eb843..57e0dc1363 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1188,6 +1188,10 @@ CREATE TABLE billing_addresses ( and second a period (or range column) that qualifies when the row applies. So a temporal primary permits multiple rows with equal values in the ordinary key parts, as long as those rows don't have overlapping periods. Each row makes a statement about the entity identified by the ordinary key parts, but applying only to the span given by the period. Temporal primary keys are essentially exclusion constraints where the first key parts are compared for equality and the last part for overlaps. + + + Any table with a temporal primary key supports temporal UPDATE and DELETE using FOR PORTION OF syntax. See and for details. + diff --git a/doc/src/sgml/ref/delete.sgml b/doc/src/sgml/ref/delete.sgml index 1b81b4e7d7..2f052f80ae 100644 --- a/doc/src/sgml/ref/delete.sgml +++ b/doc/src/sgml/ref/delete.sgml @@ -23,6 +23,7 @@ PostgreSQL documentation [ WITH [ RECURSIVE ] with_query [, ...] ] DELETE FROM [ ONLY ] table_name [ * ] [ [ AS ] alias ] + [ FOR PORTION OF range_or_period_name FROM start_time TO end_time ] [ USING from_item [, ...] ] [ WHERE condition | WHERE CURRENT OF cursor_name ] [ RETURNING * | output_expression [ [ AS ] output_name ] [, ...] ] @@ -54,6 +55,15 @@ DELETE FROM [ ONLY ] table_name [ * circumstances. + + If the table has a temporal + primary key, you may supply a + FOR PORTION OF clause, and your delete will only affect rows + that overlap the given interval. Furthermore, if a row's span extends outside + the FOR PORTION OF bounds, then new rows spanning the "cut + off" duration will be inserted to preserve the old values. + + The optional RETURNING clause causes DELETE to compute and return value(s) based on each row actually deleted. @@ -116,6 +126,42 @@ DELETE FROM [ ONLY ] table_name [ * + + range_or_period_name + + + The range column or period to use when performing a temporal delete. This + must match the range or period used in the table's temporal primary key. + + + + + + start_time + + + The earliest time (inclusive) to change in a temporal delete. + This must be a value matching the base type of the range or period from + range_or_period_name. It may also + be the special value MINVALUE to indicate a delete whose + beginning is unbounded. + + + + + + end_time + + + The latest time (exclusive) to change in a temporal delete. + This must be a value matching the base type of the range or period from + range_or_period_name. It may also + be the special value MAXVALUE to indicate a delete whose + end is unbounded. + + + + from_item diff --git a/doc/src/sgml/ref/update.sgml b/doc/src/sgml/ref/update.sgml index 3fa54e5f70..8baa13daf9 100644 --- a/doc/src/sgml/ref/update.sgml +++ b/doc/src/sgml/ref/update.sgml @@ -23,6 +23,7 @@ PostgreSQL documentation [ WITH [ RECURSIVE ] with_query [, ...] ] UPDATE [ ONLY ] table_name [ * ] [ [ AS ] alias ] + [ FOR PORTION OF range_or_period_name FROM start_time TO end_time ] SET { column_name = { expression | DEFAULT } | ( column_name [, ...] ) = [ ROW ] ( { expression | DEFAULT } [, ...] ) | ( column_name [, ...] ) = ( sub-SELECT ) @@ -51,6 +52,16 @@ UPDATE [ ONLY ] table_name [ * ] [ circumstances. + + If the table has a temporal + primary key, you may supply a + FOR PORTION OF clause, and your update will only affect rows + that overlap the given interval. Furthermore, if a row's span extends outside + the FOR PORTION OF bounds, then it will be truncated to fit + within the bounds, and new rows spanning the "cut off" duration will be + inserted to preserve the old values. + + The optional RETURNING clause causes UPDATE to compute and return value(s) based on each row actually updated. @@ -114,6 +125,42 @@ UPDATE [ ONLY ] table_name [ * ] [ + + range_or_period_name + + + The range column or period to use when performing a temporal update. This + must match the range or period used in the table's temporal primary key. + + + + + + start_time + + + The earliest time (inclusive) to change in a temporal update. + This must be a value matching the base type of the range or period from + range_or_period_name. It may also + be the special value MINVALUE to indicate an update whose + beginning is unbounded. + + + + + + end_time + + + The latest time (exclusive) to change in a temporal update. + This must be a value matching the base type of the range or period from + range_or_period_name. It may also + be the special value MAXVALUE to indicate an update whose + end is unbounded. + + + + column_name diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml index 7e2654493b..072152980d 100644 --- a/doc/src/sgml/trigger.sgml +++ b/doc/src/sgml/trigger.sgml @@ -515,17 +515,18 @@ CALLED_AS_TRIGGER(fcinfo) typedef struct TriggerData { - NodeTag type; - TriggerEvent tg_event; - Relation tg_relation; - HeapTuple tg_trigtuple; - HeapTuple tg_newtuple; - Trigger *tg_trigger; - TupleTableSlot *tg_trigslot; - TupleTableSlot *tg_newslot; - Tuplestorestate *tg_oldtable; - Tuplestorestate *tg_newtable; - const Bitmapset *tg_updatedcols; + NodeTag type; + TriggerEvent tg_event; + Relation tg_relation; + HeapTuple tg_trigtuple; + HeapTuple tg_newtuple; + Trigger *tg_trigger; + TupleTableSlot *tg_trigslot; + TupleTableSlot *tg_newslot; + Tuplestorestate *tg_oldtable; + Tuplestorestate *tg_newtable; + const Bitmapset *tg_updatedcols; + ForPortionOfState *tg_temporal; } TriggerData; @@ -792,6 +793,43 @@ typedef struct Trigger + + + tg_temporal + + + Set for UPDATE and DELETE queries + that use FOR PORTION OF, otherwise NULL. + Contains a pointer to a structure of type + ForPortionOfState, defined in + nodes/execnodes.h: + + +typedef struct ForPortionOfState +{ + NodeTag type; + + char *fp_rangeName; /* the column/PERIOD named in FOR PORTION OF */ + Oid fp_rangeType; /* the type of the FOR PORTION OF expression */ + bool fp_hasPeriod; /* true iff this is a PERIOD not a range */ + char *fp_periodStartName; /* the PERIOD's start column */ + char *fp_periodEndName; /* the PERIOD's end column */ + Datum fp_targetRange; /* the range from FOR PORTION OF */ +} ForPortionOfState; + + + where fp_rangeName is the period or range + column named in the FOR PORTION OF clause, + fp_rangeType is its range type, + fp_hasPeriod indicates whether a period was used + or a range column, fp_periodStartName and + fp_periodEndName are the names of the period's + start and end columns (or NULL if a range column was used), + and fp_targetRange is a rangetype value created + by evaluating the FOR PORTION OF bounds. + + + diff --git a/src/backend/access/brin/brin_minmax_multi.c b/src/backend/access/brin/brin_minmax_multi.c index a85dfdfec4..b4f5a058b7 100644 --- a/src/backend/access/brin/brin_minmax_multi.c +++ b/src/backend/access/brin/brin_minmax_multi.c @@ -198,7 +198,7 @@ typedef struct Ranges * with basic metadata, followed by the boundary values. It has a varlena * header, so can be treated as varlena directly. * - * See range_serialize/range_deserialize for serialization details. + * See brin_range_serialize/brin_range_deserialize for serialization details. */ typedef struct SerializedRanges { @@ -217,9 +217,9 @@ typedef struct SerializedRanges char data[FLEXIBLE_ARRAY_MEMBER]; } SerializedRanges; -static SerializedRanges *range_serialize(Ranges *range); +static SerializedRanges *brin_range_serialize(Ranges *range); -static Ranges *range_deserialize(int maxvalues, SerializedRanges *range); +static Ranges *brin_range_deserialize(int maxvalues, SerializedRanges *range); /* @@ -566,14 +566,14 @@ range_deduplicate_values(Ranges *range) /* - * range_serialize + * brin_range_serialize * Serialize the in-memory representation into a compact varlena value. * * Simply copy the header and then also the individual values, as stored * in the in-memory value array. */ static SerializedRanges * -range_serialize(Ranges *range) +brin_range_serialize(Ranges *range) { Size len; int nvalues; @@ -712,14 +712,14 @@ range_serialize(Ranges *range) } /* - * range_deserialize + * brin_range_deserialize * Serialize the in-memory representation into a compact varlena value. * * Simply copy the header and then also the individual values, as stored * in the in-memory value array. */ static Ranges * -range_deserialize(int maxvalues, SerializedRanges *serialized) +brin_range_deserialize(int maxvalues, SerializedRanges *serialized) { int i, nvalues; @@ -2405,7 +2405,7 @@ brin_minmax_multi_serialize(BrinDesc *bdesc, Datum src, Datum *dst) /* At this point everything has to be fully sorted. */ Assert(ranges->nsorted == ranges->nvalues); - s = range_serialize(ranges); + s = brin_range_serialize(ranges); dst[0] = PointerGetDatum(s); } @@ -2528,7 +2528,7 @@ brin_minmax_multi_add_value(PG_FUNCTION_ARGS) maxvalues = Max(maxvalues, MINMAX_BUFFER_MIN); maxvalues = Min(maxvalues, MINMAX_BUFFER_MAX); - ranges = range_deserialize(maxvalues, serialized); + ranges = brin_range_deserialize(maxvalues, serialized); ranges->attno = attno; ranges->colloid = colloid; @@ -2581,7 +2581,7 @@ brin_minmax_multi_consistent(PG_FUNCTION_ARGS) attno = column->bv_attno; serialized = (SerializedRanges *) PG_DETOAST_DATUM(column->bv_values[0]); - ranges = range_deserialize(serialized->maxvalues, serialized); + ranges = brin_range_deserialize(serialized->maxvalues, serialized); /* inspect the ranges, and for each one evaluate the scan keys */ for (rangeno = 0; rangeno < ranges->nranges; rangeno++) @@ -2776,8 +2776,8 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) serialized_a = (SerializedRanges *) PG_DETOAST_DATUM(col_a->bv_values[0]); serialized_b = (SerializedRanges *) PG_DETOAST_DATUM(col_b->bv_values[0]); - ranges_a = range_deserialize(serialized_a->maxvalues, serialized_a); - ranges_b = range_deserialize(serialized_b->maxvalues, serialized_b); + ranges_a = brin_range_deserialize(serialized_a->maxvalues, serialized_a); + ranges_b = brin_range_deserialize(serialized_b->maxvalues, serialized_b); /* make sure neither of the ranges is NULL */ Assert(ranges_a && ranges_b); @@ -2859,7 +2859,7 @@ brin_minmax_multi_union(PG_FUNCTION_ARGS) /* cleanup and update the serialized value */ pfree(serialized_a); - col_a->bv_values[0] = PointerGetDatum(range_serialize(ranges_a)); + col_a->bv_values[0] = PointerGetDatum(brin_range_serialize(ranges_a)); PG_RETURN_VOID(); } @@ -3041,7 +3041,7 @@ brin_minmax_multi_summary_out(PG_FUNCTION_ARGS) fmgr_info(outfunc, &fmgrinfo); /* deserialize the range info easy-to-process pieces */ - ranges_deserialized = range_deserialize(ranges->maxvalues, ranges); + ranges_deserialized = brin_range_deserialize(ranges->maxvalues, ranges); appendStringInfo(&str, "nranges: %d nvalues: %d maxvalues: %d", ranges_deserialized->nranges, diff --git a/src/backend/commands/indexcmds.c b/src/backend/commands/indexcmds.c index 670024762e..91455261ce 100644 --- a/src/backend/commands/indexcmds.c +++ b/src/backend/commands/indexcmds.c @@ -1161,6 +1161,12 @@ DefineIndex(Oid relationId, ObjectAddressSet(address, RelationRelationId, indexRelationId); + /* + * If we created a temporal PK, create triggers for FOR PORTION OF queries. + */ + if (stmt->primary && stmt->istemporal) + CreateTemporalPrimaryKeyTriggers(rel, createdConstraintId, indexRelationId); + /* * Revert to original default_tablespace. Must do this before any return * from this function, but after index_create, so this is a good time. diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 469f2042f1..88b692bd8d 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -4247,6 +4247,67 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse, AlterTableUtilityContext ATController(NULL, rel, cmds, recurse, lockmode, context); } +/* + * CreateTemporalPrimaryKeyTriggers + * Create the triggers to perform implicit INSERTs in FOR PORTION OF + * queries. + */ +void +CreateTemporalPrimaryKeyTriggers(Relation rel, Oid constraintOid, Oid indexOid) +{ + CreateTrigStmt *pk_trigger; + + /* + * Build and execute a CREATE TRIGGER statement AFTER UPDATE. + */ + pk_trigger = makeNode(CreateTrigStmt); + pk_trigger->trigname = "PK_TemporalTrigger"; + pk_trigger->relation = NULL; + pk_trigger->row = true; + pk_trigger->timing = TRIGGER_TYPE_AFTER; + pk_trigger->events = TRIGGER_TYPE_UPDATE; + pk_trigger->columns = NIL; + pk_trigger->transitionRels = NIL; + pk_trigger->whenClause = NULL; + pk_trigger->isconstraint = false; + pk_trigger->constrrel = NULL; + pk_trigger->deferrable = false; + pk_trigger->initdeferred = false; + pk_trigger->funcname = SystemFuncName("FP_insert_leftovers"); + pk_trigger->args = NIL; + + (void) CreateTrigger(pk_trigger, NULL, RelationGetRelid(rel), InvalidOid, + constraintOid, + indexOid, InvalidOid, InvalidOid, NULL, true, false); + + /* Make changes-so-far visible */ + CommandCounterIncrement(); + + /* + * Build and execute a CREATE TRIGGER statement AFTER DELETE. + */ + pk_trigger = makeNode(CreateTrigStmt); + pk_trigger->trigname = "PK_TemporalTrigger"; + pk_trigger->relation = NULL; + pk_trigger->row = true; + pk_trigger->timing = TRIGGER_TYPE_AFTER; + pk_trigger->events = TRIGGER_TYPE_DELETE; + pk_trigger->columns = NIL; + pk_trigger->transitionRels = NIL; + pk_trigger->whenClause = NULL; + pk_trigger->isconstraint = false; + pk_trigger->constrrel = NULL; + pk_trigger->deferrable = false; + pk_trigger->initdeferred = false; + pk_trigger->funcname = SystemFuncName("FP_insert_leftovers"); + pk_trigger->args = NIL; + + (void) CreateTrigger(pk_trigger, NULL, RelationGetRelid(rel), InvalidOid, + constraintOid, + indexOid, InvalidOid, InvalidOid, NULL, true, false); + +} + /* * AlterTableGetLockLevel * @@ -9162,6 +9223,9 @@ ATExecAddIndexConstraint(AlteredTableInfo *tab, Relation rel, allowSystemTableMods, false); /* is_internal */ + if (stmt->primary && stmt->istemporal) + CreateTemporalPrimaryKeyTriggers(rel, address.objectId, index_oid); + index_close(indexRel, NoLock); return address; @@ -11698,6 +11762,7 @@ validateForeignKeyConstraint(char *conname, trigdata.tg_trigtuple = ExecFetchSlotHeapTuple(slot, false, NULL); trigdata.tg_trigslot = slot; trigdata.tg_trigger = &trig; + trigdata.tg_temporal = NULL; fcinfo->context = (Node *) &trigdata; diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index 313ba9cdf6..a503d638be 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -54,6 +54,7 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/bytea.h" +#include "utils/datum.h" #include "utils/fmgroids.h" #include "utils/inval.h" #include "utils/lsyscache.h" @@ -2558,6 +2559,7 @@ ExecBSDeleteTriggers(EState *estate, ResultRelInfo *relinfo) LocTriggerData.tg_event = TRIGGER_EVENT_DELETE | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { Trigger *trigger = &trigdesc->triggers[i]; @@ -2653,6 +2655,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, TRIGGER_EVENT_ROW | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { HeapTuple newtuple; @@ -2734,6 +2737,7 @@ ExecIRDeleteTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_ROW | TRIGGER_EVENT_INSTEAD; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; ExecForceStoreHeapTuple(trigtuple, slot, false); @@ -2797,6 +2801,7 @@ ExecBSUpdateTriggers(EState *estate, ResultRelInfo *relinfo) TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; LocTriggerData.tg_updatedcols = updatedCols; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; for (i = 0; i < trigdesc->numtriggers; i++) { Trigger *trigger = &trigdesc->triggers[i]; @@ -2909,6 +2914,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, TRIGGER_EVENT_ROW | TRIGGER_EVENT_BEFORE; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; updatedCols = ExecGetAllUpdatedCols(relinfo, estate); LocTriggerData.tg_updatedcols = updatedCols; for (i = 0; i < trigdesc->numtriggers; i++) @@ -3032,6 +3038,7 @@ ExecIRUpdateTriggers(EState *estate, ResultRelInfo *relinfo, TRIGGER_EVENT_ROW | TRIGGER_EVENT_INSTEAD; LocTriggerData.tg_relation = relinfo->ri_RelationDesc; + LocTriggerData.tg_temporal = relinfo->ri_forPortionOf; ExecForceStoreHeapTuple(trigtuple, oldslot, false); @@ -3470,6 +3477,7 @@ typedef struct AfterTriggerSharedData Oid ats_tgoid; /* the trigger's ID */ Oid ats_relid; /* the relation it's on */ CommandId ats_firing_id; /* ID for firing cycle */ + ForPortionOfState *for_portion_of; /* the FOR PORTION OF clause */ struct AfterTriggersTableData *ats_table; /* transition table access */ Bitmapset *ats_modifiedcols; /* modified columns */ } AfterTriggerSharedData; @@ -3698,6 +3706,7 @@ static SetConstraintState SetConstraintStateCreate(int numalloc); static SetConstraintState SetConstraintStateCopy(SetConstraintState state); static SetConstraintState SetConstraintStateAddItem(SetConstraintState state, Oid tgoid, bool tgisdeferred); +static ForPortionOfState *CopyForPortionOfState(ForPortionOfState *src); static void cancel_prior_stmt_triggers(Oid relid, CmdType cmdType, int tgevent); @@ -4175,6 +4184,7 @@ AfterTriggerExecute(EState *estate, LocTriggerData.tg_relation = rel; if (TRIGGER_FOR_UPDATE(LocTriggerData.tg_trigger->tgtype)) LocTriggerData.tg_updatedcols = evtshared->ats_modifiedcols; + LocTriggerData.tg_temporal = evtshared->for_portion_of; MemoryContextReset(per_tuple_context); @@ -5571,6 +5581,50 @@ AfterTriggerPendingOnRel(Oid relid) return false; } +/* ---------- + * ForPortionOfState() + * + * Copies a ForPortionOfState into the current memory context. + */ +static ForPortionOfState * +CopyForPortionOfState(ForPortionOfState *src) +{ + ForPortionOfState *dst = NULL; + if (src) { + MemoryContext oldctx; + RangeType *r; + TypeCacheEntry *typcache; + + /* + * Need to lift the FOR PORTION OF details into a higher memory context + * because cascading foreign key update/deletes can cause triggers to fire + * triggers (both other TRI triggers and the insert_leftovers trigger), + * and the AfterTriggerEvents will outlive the FPO details of the original + * query. + */ + oldctx = MemoryContextSwitchTo(TopTransactionContext); + dst = makeNode(ForPortionOfState); + dst->fp_rangeName = pstrdup(src->fp_rangeName); + dst->fp_rangeType = src->fp_rangeType; + dst->fp_hasPeriod = src->fp_hasPeriod; + + if (src->fp_periodStartName) + dst->fp_periodStartName = pstrdup(src->fp_periodStartName); + else + dst->fp_periodStartName = NULL; + + if (src->fp_periodEndName) + dst->fp_periodEndName = pstrdup(src->fp_periodEndName); + else + dst->fp_periodEndName = NULL; + + r = DatumGetRangeTypeP(src->fp_targetRange); + typcache = lookup_type_cache(RangeTypeGetOid(r), TYPECACHE_RANGE_INFO); + dst->fp_targetRange = datumCopy(src->fp_targetRange, typcache->typbyval, typcache->typlen); + MemoryContextSwitchTo(oldctx); + } + return dst; +} /* ---------- * AfterTriggerSaveEvent() @@ -5890,6 +5944,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo, else new_shared.ats_table = NULL; new_shared.ats_modifiedcols = modifiedCols; + new_shared.for_portion_of = CopyForPortionOfState(relinfo->ri_forPortionOf); afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth].events, &new_event, &new_shared); diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index b3ce4bae53..80a3cdfc22 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -1240,6 +1240,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo, resultRelInfo->ri_projectReturning = NULL; resultRelInfo->ri_onConflictArbiterIndexes = NIL; resultRelInfo->ri_onConflict = NULL; + resultRelInfo->ri_forPortionOf = NULL; resultRelInfo->ri_ReturningSlot = NULL; resultRelInfo->ri_TrigOldSlot = NULL; resultRelInfo->ri_TrigNewSlot = NULL; diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index d328856ae5..e7b9d88d39 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -51,6 +51,8 @@ #include "utils/builtins.h" #include "utils/datum.h" #include "utils/memutils.h" +#include "utils/rangetypes.h" +#include "utils/lsyscache.h" #include "utils/rel.h" @@ -3048,6 +3050,37 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) } } + /* + * If needed, initialize the target range for FOR PORTION OF. + */ + if (node->forPortionOf) + { + ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf; + Datum targetRange; + bool isNull; + ExprContext *econtext; + ExprState *exprState; + + /* Eval the FOR PORTION OF target */ + if (mtstate->ps.ps_ExprContext == NULL) + ExecAssignExprContext(estate, &mtstate->ps); + econtext = mtstate->ps.ps_ExprContext; + + exprState = ExecPrepareExpr((Expr *) forPortionOf->targetRange, estate); + targetRange = ExecEvalExpr(exprState, econtext, &isNull); + + if (isNull) elog(ERROR, "Got a NULL FOR PORTION OF target range"); + + resultRelInfo->ri_forPortionOf = makeNode(ForPortionOfState); + resultRelInfo->ri_forPortionOf->fp_rangeName = forPortionOf->range_name; + resultRelInfo->ri_forPortionOf->fp_periodStartName = forPortionOf->period_start_name; + resultRelInfo->ri_forPortionOf->fp_periodEndName = forPortionOf->period_end_name; + resultRelInfo->ri_forPortionOf->fp_targetRange = targetRange; + resultRelInfo->ri_forPortionOf->fp_rangeType = forPortionOf->rangeType; + + /* Don't free the ExprContext here because the result must last for the whole query */ + } + /* * If we have any secondary relations in an UPDATE or DELETE, they need to * be treated like non-locked relations in SELECT FOR UPDATE, ie, the diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index f476009d37..833212fb7f 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -222,6 +222,7 @@ _copyModifyTable(const ModifyTable *from) COPY_NODE_FIELD(rowMarks); COPY_SCALAR_FIELD(epqParam); COPY_SCALAR_FIELD(onConflictAction); + COPY_NODE_FIELD(forPortionOf); COPY_NODE_FIELD(arbiterIndexes); COPY_NODE_FIELD(onConflictSet); COPY_NODE_FIELD(onConflictCols); @@ -2295,6 +2296,33 @@ _copyOnConflictExpr(const OnConflictExpr *from) return newnode; } +/* + * _copyForPortionOfExpr + */ +static ForPortionOfExpr * +_copyForPortionOfExpr(const ForPortionOfExpr *from) +{ + ForPortionOfExpr *newnode = makeNode(ForPortionOfExpr); + + COPY_SCALAR_FIELD(range_attno); + COPY_SCALAR_FIELD(start_attno); + COPY_SCALAR_FIELD(end_attno); + COPY_STRING_FIELD(range_name); + COPY_STRING_FIELD(period_start_name); + COPY_STRING_FIELD(period_end_name); + COPY_NODE_FIELD(range); + COPY_NODE_FIELD(startCol); + COPY_NODE_FIELD(endCol); + COPY_NODE_FIELD(targetStart); + COPY_NODE_FIELD(targetEnd); + COPY_NODE_FIELD(targetRange); + COPY_SCALAR_FIELD(rangeType); + COPY_NODE_FIELD(overlapsExpr); + COPY_NODE_FIELD(rangeSet); + + return newnode; +} + /* **************************************************************** * pathnodes.h copy functions * @@ -2671,6 +2699,19 @@ _copyCTECycleClause(const CTECycleClause *from) return newnode; } +static ForPortionOfClause * +_copyForPortionOfClause(const ForPortionOfClause *from) +{ + ForPortionOfClause *newnode = makeNode(ForPortionOfClause); + + COPY_STRING_FIELD(range_name); + COPY_SCALAR_FIELD(range_name_location); + COPY_NODE_FIELD(target_start); + COPY_NODE_FIELD(target_end); + + return newnode; +} + static CommonTableExpr * _copyCommonTableExpr(const CommonTableExpr *from) { @@ -5338,6 +5379,9 @@ copyObjectImpl(const void *from) case T_OnConflictExpr: retval = _copyOnConflictExpr(from); break; + case T_ForPortionOfExpr: + retval = _copyForPortionOfExpr(from); + break; /* * RELATION NODES @@ -5889,6 +5933,9 @@ copyObjectImpl(const void *from) case T_CTECycleClause: retval = _copyCTECycleClause(from); break; + case T_ForPortionOfClause: + retval = _copyForPortionOfClause(from); + break; case T_CommonTableExpr: retval = _copyCommonTableExpr(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 8c8e4c5ffc..9e23b8cc43 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -841,6 +841,24 @@ _equalOnConflictExpr(const OnConflictExpr *a, const OnConflictExpr *b) return true; } +static bool +_equalForPortionOfExpr(const ForPortionOfExpr *a, const ForPortionOfExpr *b) +{ + COMPARE_SCALAR_FIELD(range_attno); + COMPARE_STRING_FIELD(range_name); + COMPARE_NODE_FIELD(range); + COMPARE_NODE_FIELD(startCol); + COMPARE_NODE_FIELD(endCol); + COMPARE_NODE_FIELD(targetStart); + COMPARE_NODE_FIELD(targetEnd); + COMPARE_NODE_FIELD(targetRange); + COMPARE_SCALAR_FIELD(rangeType); + COMPARE_NODE_FIELD(overlapsExpr); + COMPARE_NODE_FIELD(rangeSet); + + return true; +} + /* * Stuff from pathnodes.h */ @@ -2964,6 +2982,17 @@ _equalCTECycleClause(const CTECycleClause *a, const CTECycleClause *b) return true; } +static bool +_equalForPortionOfClause(const ForPortionOfClause *a, const ForPortionOfClause *b) +{ + COMPARE_STRING_FIELD(range_name); + COMPARE_SCALAR_FIELD(range_name_location); + COMPARE_NODE_FIELD(target_start); + COMPARE_NODE_FIELD(target_end); + + return true; +} + static bool _equalCommonTableExpr(const CommonTableExpr *a, const CommonTableExpr *b) { @@ -3349,6 +3378,9 @@ equal(const void *a, const void *b) case T_OnConflictExpr: retval = _equalOnConflictExpr(a, b); break; + case T_ForPortionOfExpr: + retval = _equalForPortionOfExpr(a, b); + break; case T_JoinExpr: retval = _equalJoinExpr(a, b); break; @@ -3890,6 +3922,9 @@ equal(const void *a, const void *b) case T_CTECycleClause: retval = _equalCTECycleClause(a, b); break; + case T_ForPortionOfClause: + retval = _equalForPortionOfClause(a, b); + break; case T_CommonTableExpr: retval = _equalCommonTableExpr(a, b); break; diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index d4c4e90c29..38512058ca 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2237,6 +2237,14 @@ expression_tree_walker(Node *node, return true; } break; + case T_ForPortionOfExpr: + { + ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node; + + if (walker((Node *) forPortionOf->targetRange, context)) + return true; + } + break; case T_PartitionPruneStepOp: { PartitionPruneStepOp *opstep = (PartitionPruneStepOp *) node; @@ -2373,6 +2381,8 @@ query_tree_walker(Query *query, return true; if (walker((Node *) query->withCheckOptions, context)) return true; + if (walker((Node *) query->forPortionOf, context)) + return true; if (walker((Node *) query->onConflict, context)) return true; if (walker((Node *) query->returningList, context)) diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index ee03b54ddc..51a26fadae 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -423,10 +423,12 @@ _outModifyTable(StringInfo str, const ModifyTable *node) WRITE_NODE_FIELD(rowMarks); WRITE_INT_FIELD(epqParam); WRITE_ENUM_FIELD(onConflictAction, OnConflictAction); + WRITE_NODE_FIELD(forPortionOf); WRITE_NODE_FIELD(arbiterIndexes); WRITE_NODE_FIELD(onConflictSet); WRITE_NODE_FIELD(onConflictCols); WRITE_NODE_FIELD(onConflictWhere); + // TODO: add things for ForPortionOf WRITE_UINT_FIELD(exclRelRTI); WRITE_NODE_FIELD(exclRelTlist); } @@ -1748,6 +1750,24 @@ _outOnConflictExpr(StringInfo str, const OnConflictExpr *node) WRITE_NODE_FIELD(exclRelTlist); } +static void +_outForPortionOfExpr(StringInfo str, const ForPortionOfExpr *node) +{ + WRITE_NODE_TYPE("FORPORTIONOFEXPR"); + + WRITE_INT_FIELD(range_attno); + WRITE_STRING_FIELD(range_name); + WRITE_NODE_FIELD(range); + WRITE_NODE_FIELD(startCol); + WRITE_NODE_FIELD(endCol); + WRITE_NODE_FIELD(targetStart); + WRITE_NODE_FIELD(targetEnd); + WRITE_NODE_FIELD(targetRange); + WRITE_OID_FIELD(rangeType); + WRITE_NODE_FIELD(overlapsExpr); + WRITE_NODE_FIELD(rangeSet); +} + /***************************************************************************** * * Stuff from pathnodes.h. @@ -4198,6 +4218,9 @@ outNode(StringInfo str, const void *obj) case T_OnConflictExpr: _outOnConflictExpr(str, obj); break; + case T_ForPortionOfExpr: + _outForPortionOfExpr(str, obj); + break; case T_Path: _outPath(str, obj); break; diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index abf08b7a2f..8f0f5a0f0d 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -1422,6 +1422,28 @@ _readAppendRelInfo(void) * Stuff from parsenodes.h. */ +/* + * _readForPortionOfExpr + */ +static ForPortionOfExpr * +_readForPortionOfExpr(void) +{ + READ_LOCALS(ForPortionOfExpr); + + READ_INT_FIELD(range_attno); + READ_STRING_FIELD(range_name); + READ_NODE_FIELD(range); + READ_NODE_FIELD(startCol); + READ_NODE_FIELD(endCol); + READ_NODE_FIELD(targetStart); + READ_NODE_FIELD(targetEnd); + READ_NODE_FIELD(targetRange); + READ_OID_FIELD(rangeType); + READ_NODE_FIELD(overlapsExpr); + READ_NODE_FIELD(rangeSet); + READ_DONE(); +} + /* * _readRangeTblEntry */ @@ -1696,6 +1718,7 @@ _readModifyTable(void) READ_NODE_FIELD(rowMarks); READ_INT_FIELD(epqParam); READ_ENUM_FIELD(onConflictAction, OnConflictAction); + READ_NODE_FIELD(forPortionOf); READ_NODE_FIELD(arbiterIndexes); READ_NODE_FIELD(onConflictSet); READ_NODE_FIELD(onConflictCols); @@ -2847,6 +2870,8 @@ parseNodeString(void) return_value = _readAppendRelInfo(); else if (MATCH("RANGETBLENTRY", 13)) return_value = _readRangeTblEntry(); + else if (MATCH("FORPORTIONOFEXPR", 16)) + return_value = _readForPortionOfExpr(); else if (MATCH("RANGETBLFUNCTION", 16)) return_value = _readRangeTblFunction(); else if (MATCH("TABLESAMPLECLAUSE", 17)) diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 3dc0176a51..a25c5ad51a 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -308,7 +308,8 @@ static ModifyTable *make_modifytable(PlannerInfo *root, Plan *subplan, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, - List *rowMarks, OnConflictExpr *onconflict, int epqParam); + List *rowMarks, OnConflictExpr *onconflict, + ForPortionOfExpr *forPortionOf, int epqParam); static GatherMerge *create_gather_merge_plan(PlannerInfo *root, GatherMergePath *best_path); @@ -2753,6 +2754,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path) best_path->returningLists, best_path->rowMarks, best_path->onconflict, + best_path->forPortionOf, best_path->epqParam); copy_generic_path_info(&plan->plan, &best_path->path); @@ -6877,7 +6879,8 @@ make_modifytable(PlannerInfo *root, Plan *subplan, List *resultRelations, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, - List *rowMarks, OnConflictExpr *onconflict, int epqParam) + List *rowMarks, OnConflictExpr *onconflict, + ForPortionOfExpr *forPortionOf, int epqParam) { ModifyTable *node = makeNode(ModifyTable); List *fdw_private_list; @@ -6942,6 +6945,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan, node->exclRelTlist = onconflict->exclRelTlist; } node->updateColnosLists = updateColnosLists; + node->forPortionOf = (Node *) forPortionOf; node->withCheckOptionLists = withCheckOptionLists; node->returningLists = returningLists; node->rowMarks = rowMarks; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index bd01ec0526..5e2d09e3eb 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -1859,6 +1859,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction) returningLists, rowMarks, parse->onConflict, + parse->forPortionOf, assign_special_exec_param(root)); } diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index e53d381e19..591528dfd1 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -3630,7 +3630,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, - int epqParam) + ForPortionOfExpr *forPortionOf, int epqParam) { ModifyTablePath *pathnode = makeNode(ModifyTablePath); @@ -3695,6 +3695,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel, pathnode->returningLists = returningLists; pathnode->rowMarks = rowMarks; pathnode->onconflict = onconflict; + pathnode->forPortionOf = forPortionOf; pathnode->epqParam = epqParam; return pathnode; diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 146ee8dd1e..230226fbc8 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -25,6 +25,7 @@ #include "postgres.h" #include "access/sysattr.h" +#include "catalog/pg_period.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "miscadmin.h" @@ -44,12 +45,14 @@ #include "parser/parse_relation.h" #include "parser/parse_target.h" #include "parser/parse_type.h" +#include "parser/parser.h" #include "parser/parsetree.h" #include "rewrite/rewriteManip.h" #include "utils/backend_status.h" #include "utils/builtins.h" #include "utils/guc.h" #include "utils/queryjumble.h" +#include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -65,6 +68,10 @@ static List *transformInsertRow(ParseState *pstate, List *exprlist, bool strip_indirection); static OnConflictExpr *transformOnConflictClause(ParseState *pstate, OnConflictClause *onConflictClause); +static Node *transformForPortionOfBound(Node *n, bool isLowerBound); +static ForPortionOfExpr *transformForPortionOfClause(ParseState *pstate, + int rtindex, + ForPortionOfClause *forPortionOfClause); static int count_rowexpr_columns(ParseState *pstate, Node *expr); static Query *transformSelectStmt(ParseState *pstate, SelectStmt *stmt); static Query *transformValuesClause(ParseState *pstate, SelectStmt *stmt); @@ -77,7 +84,8 @@ static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt); static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt); static List *transformReturningList(ParseState *pstate, List *returningList); static List *transformUpdateTargetList(ParseState *pstate, - List *targetList); + List *targetList, + ForPortionOfExpr *forPortionOf); static Query *transformPLAssignStmt(ParseState *pstate, PLAssignStmt *stmt); static Query *transformDeclareCursorStmt(ParseState *pstate, @@ -430,6 +438,7 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) { Query *qry = makeNode(Query); ParseNamespaceItem *nsitem; + Node *whereClause; Node *qual; qry->commandType = CMD_DELETE; @@ -468,7 +477,20 @@ transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt) nsitem->p_lateral_only = false; nsitem->p_lateral_ok = true; - qual = transformWhereClause(pstate, stmt->whereClause, + if (stmt->forPortionOf) + qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf); + + // TODO: DRY with UPDATE + if (stmt->forPortionOf) + { + if (stmt->whereClause) + whereClause = (Node *) makeBoolExpr(AND_EXPR, list_make2(qry->forPortionOf->overlapsExpr, stmt->whereClause), -1); + else + whereClause = qry->forPortionOf->overlapsExpr; + } + else + whereClause = stmt->whereClause; + qual = transformWhereClause(pstate, whereClause, EXPR_KIND_WHERE, "WHERE"); qry->returningList = transformReturningList(pstate, stmt->returningList); @@ -1085,7 +1107,7 @@ transformOnConflictClause(ParseState *pstate, * Now transform the UPDATE subexpressions. */ onConflictSet = - transformUpdateTargetList(pstate, onConflictClause->targetList); + transformUpdateTargetList(pstate, onConflictClause->targetList, NULL); onConflictWhere = transformWhereClause(pstate, onConflictClause->whereClause, @@ -1115,6 +1137,339 @@ transformOnConflictClause(ParseState *pstate, return result; } +/* + * transformForPortionOfBound + * transforms MINVALUE and MAXVALUE pseudo-column references to NULL + * (which represent "unbounded" in a range type, otherwise returns + * its input unchanged. + */ +static Node * +transformForPortionOfBound(Node *n, bool isLowerBound) +{ + if (nodeTag(n) == T_ColumnRef) + { + ColumnRef *cref = (ColumnRef *) n; + char *cname = ""; + A_Const *n2; + + if (list_length(cref->fields) == 1 && + IsA(linitial(cref->fields), String)) + cname = strVal(linitial(cref->fields)); + + if (strcmp("minvalue", cname) == 0) + { + if (!isLowerBound) + ereport(ERROR, (errmsg("MINVALUE can only be used for the lower bound"))); + } + else if (strcmp("maxvalue", cname) == 0) + { + if (isLowerBound) + ereport(ERROR, (errmsg("MAXVALUE can only be used for the upper bound"))); + } + else + return n; + + n2 = makeNode(A_Const); + n2->isnull = true; + n2->location = ((ColumnRef *)n)->location; + + return (Node *)n2; + } + else + return n; +} + +/* + * transformForPortionOfClause + * transforms a ForPortionOfClause in an UPDATE/DELETE statement + */ +static ForPortionOfExpr * +transformForPortionOfClause(ParseState *pstate, + int rtindex, + ForPortionOfClause *forPortionOf) +{ + Relation targetrel = pstate->p_target_relation; + RangeTblEntry *target_rte = pstate->p_target_nsitem->p_rte; + char *range_name = forPortionOf->range_name; + char *range_type_name = NULL; + int range_attno = InvalidOid; + int start_attno = InvalidOid; + int end_attno = InvalidOid; + char *startcolname = NULL; + char *endcolname = NULL; + ForPortionOfExpr *result; + List *targetList; + Oid pkoid; + HeapTuple indexTuple; + Form_pg_index pk; + Node *target_start, *target_end; + FuncCall *fc; + + result = makeNode(ForPortionOfExpr); + result->range = NULL; + + /* Make sure the table has a primary key */ + pkoid = RelationGetPrimaryKeyIndex(targetrel); + if (pkoid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("relation \"%s\" does not have a temporal primary key", + RelationGetRelationName(pstate->p_target_relation)), + parser_errposition(pstate, forPortionOf->range_name_location))); + + /* Make sure the primary key is a temporal key */ + // TODO: need a lock here? + indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(pkoid)); + if (!HeapTupleIsValid(indexTuple)) /* should not happen */ + elog(ERROR, "cache lookup failed for index %u", pkoid); + pk = (Form_pg_index) GETSTRUCT(indexTuple); + + /* + * Only temporal pkey indexes have both isprimary and isexclusion. + * Checking those saves us from scanning pg_constraint + * like in RelationGetExclusionInfo. + */ + if (!(pk->indisprimary && pk->indisexclusion)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("relation \"%s\" does not have a temporal primary key", + RelationGetRelationName(pstate->p_target_relation)), + parser_errposition(pstate, forPortionOf->range_name_location))); + + /* + * First look for a range column, then look for a period. + */ + range_attno = attnameAttNum(targetrel, range_name, false); + if (range_attno != InvalidAttrNumber) + { + Form_pg_attribute attr = TupleDescAttr(targetrel->rd_att, range_attno - 1); + Var *v; + + // TODO: check attr->attisdropped ? + + /* Make sure it's a range column */ + if (!type_is_range(attr->atttypid)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("column \"%s\" of relation \"%s\" is not a range type", + range_name, + RelationGetRelationName(pstate->p_target_relation)), + parser_errposition(pstate, forPortionOf->range_name_location))); + + /* Make sure the range attribute is the last part of the pkey. */ + if (range_attno != pk->indkey.values[pk->indnkeyatts - 1]) + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("column \"%s\" is not the temporal part of the primary key for relation \"%s\"", + range_name, + RelationGetRelationName(pstate->p_target_relation)), + parser_errposition(pstate, forPortionOf->range_name_location))); + } + + v = makeVar( + rtindex, + range_attno, + attr->atttypid, + attr->atttypmod, + attr->attcollation, + 0); + v->location = forPortionOf->range_name_location; + result->range = (Expr *) v; + result->rangeType = attr->atttypid; + range_type_name = get_typname(attr->atttypid); + + } else { + Oid relid = RelationGetRelid(targetrel); + HeapTuple perTuple = SearchSysCache2(PERIODNAME, + ObjectIdGetDatum(relid), + PointerGetDatum(range_name)); + if (HeapTupleIsValid(perTuple)) + { + Form_pg_period per = (Form_pg_period) GETSTRUCT(perTuple); + Form_pg_attribute startattr, endattr; + bool pkeyIncludesPeriod = false; + Var *startvar, *endvar; + FuncCall *periodRange; + + Type rngtype = typeidType(per->perrngtype); + range_type_name = typeTypeName(rngtype); + ReleaseSysCache(rngtype); + start_attno = per->perstart; + end_attno = per->perend; + + startattr = TupleDescAttr(targetrel->rd_att, start_attno - 1); + endattr = TupleDescAttr(targetrel->rd_att, end_attno - 1); + + startcolname = NameStr(startattr->attname); + endcolname = NameStr(endattr->attname); + result->period_start_name = startcolname; + result->period_end_name = endcolname; + + /* Make sure the period is the last part of the pkey. */ + if (pk->indkey.values[pk->indnkeyatts - 1] == InvalidOid) + { + /* + * The PK ends with an expression. Make sure it's for our period. + * There should be only one entry in indexprs, and it should be ours. + */ + pkeyIncludesPeriod = pk->indperiod == per->oid; + } + + if (!pkeyIncludesPeriod) + ereport(ERROR, + (errcode(ERRCODE_INVALID_COLUMN_REFERENCE), + errmsg("column \"%s\" is not the temporal part of the primary key for relation \"%s\"", + range_name, + RelationGetRelationName(pstate->p_target_relation)), + parser_errposition(pstate, forPortionOf->range_name_location))); + + startvar = makeVar( + rtindex, + per->perstart, + startattr->atttypid, + startattr->atttypmod, + startattr->attcollation, + 0); + startvar->location = forPortionOf->range_name_location; + + endvar = makeVar( + rtindex, + per->perend, + endattr->atttypid, + endattr->atttypmod, + endattr->attcollation, + 0); + endvar->location = forPortionOf->range_name_location; + + ReleaseSysCache(perTuple); + + periodRange = makeFuncCall(SystemFuncName(range_type_name), + list_make2(startvar, endvar), + COERCE_EXPLICIT_CALL, + forPortionOf->range_name_location); + result->range = (Expr *) periodRange; + result->rangeType = typenameTypeId(pstate, typeStringToTypeName(range_type_name)); + } + } + ReleaseSysCache(indexTuple); + + if (result->range == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column or period \"%s\" of relation \"%s\" does not exist", + range_name, + RelationGetRelationName(pstate->p_target_relation)), + parser_errposition(pstate, forPortionOf->range_name_location))); + + /* + * Build a range from the FROM ... TO .... bounds. + * This should give a constant result, so we accept functions like NOW() + * but not column references, subqueries, etc. + * + * It also permits MINVALUE and MAXVALUE like declarative partitions. + */ + target_start = transformForPortionOfBound(forPortionOf->target_start, true); + target_end = transformForPortionOfBound(forPortionOf->target_end, false); + fc = makeFuncCall(SystemFuncName(range_type_name), + list_make2(target_start, target_end), + COERCE_EXPLICIT_CALL, + forPortionOf->range_name_location); + result->targetRange = transformExpr(pstate, (Node *) fc, EXPR_KIND_UPDATE_PORTION); + + /* overlapsExpr is something we can add to the whereClause */ + result->overlapsExpr = (Node *) makeSimpleA_Expr(AEXPR_OP, "&&", + // TODO: Maybe need a copy here?: + (Node *) result->range, (Node *) fc, + forPortionOf->range_name_location); + + /* + * Now make sure we update the start/end time of the record. + * For a range col (r) this is `r = r * targetRange`. + * For a PERIOD with cols (s, e) this is `s = lower(tsrange(s, e) * targetRange)` + * and `e = upper(tsrange(s, e) * targetRange` (of course not necessarily with + * tsrange, but with whatever range type is used there)). + * + * We also compute the possible left-behind bits at the start and end of the tuple, + * so that we can INSERT them if necessary. + */ + // TODO: Only do this for UPDATE, not DELETE: + targetList = NIL; + if (range_attno != InvalidAttrNumber) + { + Expr *rangeSetExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*", + // TODO: Maybe need a copy here?: + (Node *) result->range, (Node *) fc, + forPortionOf->range_name_location); + TargetEntry *tle; + + rangeSetExpr = (Expr *) transformExpr(pstate, (Node *) rangeSetExpr, EXPR_KIND_UPDATE_PORTION); + tle = makeTargetEntry(rangeSetExpr, + range_attno, + range_name, + false); + + targetList = lappend(targetList, tle); + + /* Mark the range column as requiring update permissions */ + target_rte->updatedCols = bms_add_member(target_rte->updatedCols, + range_attno - FirstLowInvalidHeapAttributeNumber); + + } else { + Expr *intersectExpr; + Expr *boundSetExpr; + TargetEntry *tle; + + /* Set up targetList for the PERIOD start column */ + + intersectExpr = (Expr *) makeSimpleA_Expr(AEXPR_OP, "*", + // TODO: copy? + (Node *) result->range, (Node *) fc, + forPortionOf->range_name_location); + + boundSetExpr = (Expr *) makeFuncCall(SystemFuncName("lower"), + list_make1(intersectExpr), + COERCE_EXPLICIT_CALL, + forPortionOf->range_name_location); + boundSetExpr = (Expr *) transformExpr(pstate, (Node *) boundSetExpr, EXPR_KIND_UPDATE_PORTION); + + tle = makeTargetEntry(boundSetExpr, + start_attno, + startcolname, + false); + + targetList = lappend(targetList, tle); + + /* Set up targetList for the PERIOD end column */ + + boundSetExpr = (Expr *) makeFuncCall(SystemFuncName("upper"), + list_make1(intersectExpr), + COERCE_EXPLICIT_CALL, + forPortionOf->range_name_location); + boundSetExpr = (Expr *) transformExpr(pstate, (Node *) boundSetExpr, EXPR_KIND_UPDATE_PORTION); + + tle = makeTargetEntry(boundSetExpr, + end_attno, + endcolname, + false); + + targetList = lappend(targetList, tle); + + /* Mark the bound columns as requiring update permissions */ + target_rte->updatedCols = bms_add_member(target_rte->updatedCols, + start_attno - FirstLowInvalidHeapAttributeNumber); + target_rte->updatedCols = bms_add_member(target_rte->updatedCols, + end_attno - FirstLowInvalidHeapAttributeNumber); + } + + result->rangeSet = targetList; + result->range_name = range_name; + result->range_attno = range_attno; + result->start_attno = start_attno; + result->end_attno = end_attno; + + return result; +} /* * BuildOnConflictExcludedTargetlist @@ -2320,6 +2675,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) { Query *qry = makeNode(Query); ParseNamespaceItem *nsitem; + Node *whereClause; Node *qual; qry->commandType = CMD_UPDATE; @@ -2337,6 +2693,10 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) stmt->relation->inh, true, ACL_UPDATE); + + if (stmt->forPortionOf) + qry->forPortionOf = transformForPortionOfClause(pstate, qry->resultRelation, stmt->forPortionOf); + nsitem = pstate->p_target_nsitem; /* subqueries in FROM cannot access the result relation */ @@ -2353,7 +2713,16 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) nsitem->p_lateral_only = false; nsitem->p_lateral_ok = true; - qual = transformWhereClause(pstate, stmt->whereClause, + if (stmt->forPortionOf) + { + if (stmt->whereClause) + whereClause = (Node *) makeBoolExpr(AND_EXPR, list_make2(qry->forPortionOf->overlapsExpr, stmt->whereClause), -1); + else + whereClause = qry->forPortionOf->overlapsExpr; + } + else + whereClause = stmt->whereClause; + qual = transformWhereClause(pstate, whereClause, EXPR_KIND_WHERE, "WHERE"); qry->returningList = transformReturningList(pstate, stmt->returningList); @@ -2362,7 +2731,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) * Now we are done with SELECT-like processing, and can get on with * transforming the target list to match the UPDATE target columns. */ - qry->targetList = transformUpdateTargetList(pstate, stmt->targetList); + qry->targetList = transformUpdateTargetList(pstate, stmt->targetList, qry->forPortionOf); qry->rtable = pstate->p_rtable; qry->jointree = makeFromExpr(pstate->p_joinlist, qual); @@ -2380,7 +2749,7 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt) * handle SET clause in UPDATE/INSERT ... ON CONFLICT UPDATE */ static List * -transformUpdateTargetList(ParseState *pstate, List *origTlist) +transformUpdateTargetList(ParseState *pstate, List *origTlist, ForPortionOfExpr *forPortionOf) { List *tlist = NIL; RangeTblEntry *target_rte; @@ -2430,6 +2799,9 @@ transformUpdateTargetList(ParseState *pstate, List *origTlist) RelationGetRelationName(pstate->p_target_relation)), parser_errposition(pstate, origTarget->location))); + /* TODO: Make sure user isn't trying to SET the range attribute directly --- TODO or permit it?? */ + + updateTargetListEntry(pstate, tle, origTarget->name, attrno, origTarget->indirection, diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 62c545f8fc..d7ffb1d7bb 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -248,6 +248,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); RangeVar *range; IntoClause *into; WithClause *with; + ForPortionOfClause *forportionof; InferClause *infer; OnConflictClause *onconflict; A_Indices *aind; @@ -525,6 +526,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type relation_expr %type extended_relation_expr %type relation_expr_opt_alias +%type for_portion_of_clause %type tablesample_clause opt_repeatable_clause %type target_el set_target insert_column_item @@ -700,7 +702,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PERIOD PLACING PLANS POLICY - POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY + PORTION POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION QUOTE @@ -773,6 +775,16 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %nonassoc '<' '>' '=' LESS_EQUALS GREATER_EQUALS NOT_EQUALS %nonassoc BETWEEN IN_P LIKE ILIKE SIMILAR NOT_LA %nonassoc ESCAPE /* ESCAPE must be just above LIKE/ILIKE/SIMILAR */ +/* + * We need to handle this shift/reduce conflict: + * FOR PORTION OF valid_at FROM INTERVAL YEAR TO MONTH TO foo. + * This is basically the classic "dangling else" problem, and we want a + * similar resolution: treat the TO as part of the INTERVAL, not as part of + * the FROM ... TO .... Users can add parentheses if that's a problem. + * TO just needs to be higher precedence than YEAR_P etc. + */ +%nonassoc YEAR_P MONTH_P DAY_P HOUR_P MINUTE_P +%nonassoc TO /* * To support target_el without AS, it used to be necessary to assign IDENT an * explicit precedence just less than Op. While that's not really necessary @@ -11255,13 +11267,15 @@ returning_clause: *****************************************************************************/ DeleteStmt: opt_with_clause DELETE_P FROM relation_expr_opt_alias + for_portion_of_clause using_clause where_or_current_clause returning_clause { DeleteStmt *n = makeNode(DeleteStmt); n->relation = $4; - n->usingClause = $5; - n->whereClause = $6; - n->returningList = $7; + n->forPortionOf = $5; + n->usingClause = $6; + n->whereClause = $7; + n->returningList = $8; n->withClause = $1; $$ = (Node *)n; } @@ -11324,6 +11338,7 @@ opt_nowait_or_skip: *****************************************************************************/ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias + for_portion_of_clause SET set_clause_list from_clause where_or_current_clause @@ -11331,10 +11346,11 @@ UpdateStmt: opt_with_clause UPDATE relation_expr_opt_alias { UpdateStmt *n = makeNode(UpdateStmt); n->relation = $3; - n->targetList = $5; - n->fromClause = $6; - n->whereClause = $7; - n->returningList = $8; + n->forPortionOf = $4; + n->targetList = $6; + n->fromClause = $7; + n->whereClause = $8; + n->returningList = $9; n->withClause = $1; $$ = (Node *)n; } @@ -12619,6 +12635,19 @@ relation_expr_opt_alias: relation_expr %prec UMINUS } ; +for_portion_of_clause: + FOR PORTION OF ColId FROM a_expr TO a_expr + { + ForPortionOfClause *n = makeNode(ForPortionOfClause); + n->range_name = $4; + n->range_name_location = @4; + n->target_start = $6; + n->target_end = $8; + $$ = n; + } + | /*EMPTY*/ { $$ = NULL; } + ; + /* * TABLESAMPLE decoration in a FROM item */ @@ -15786,6 +15815,7 @@ unreserved_keyword: | PASSWORD | PLANS | POLICY + | PORTION | PRECEDING | PREPARE | PREPARED @@ -16367,6 +16397,7 @@ bare_label_keyword: | PLACING | PLANS | POLICY + | PORTION | POSITION | PRECEDING | PREPARE diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index 7d829a05a9..41c1a8fd7c 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -555,6 +555,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_UPDATE_PORTION: + if (isAgg) + err = _("aggregate functions are not allowed in FOR PORTION OF expressions"); + else + err = _("grouping operations are not allowed in FOR PORTION OF expressions"); + + break; /* * There is intentionally no default: case here, so that the @@ -943,6 +950,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_UPDATE_PORTION: + err = _("window functions are not allowed in FOR PORTION OF expressions"); + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c index 4133526f04..9991da3b3e 100644 --- a/src/backend/parser/parse_collate.c +++ b/src/backend/parser/parse_collate.c @@ -484,6 +484,7 @@ assign_collations_walker(Node *node, assign_collations_context *context) case T_JoinExpr: case T_FromExpr: case T_OnConflictExpr: + case T_ForPortionOfExpr: case T_SortGroupClause: (void) expression_tree_walker(node, assign_collations_walker, diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 2d1a477154..cef7f3f4c9 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -513,6 +513,9 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_PARTITION_BOUND: err = _("cannot use column reference in partition bound expression"); break; + case EXPR_KIND_UPDATE_PORTION: + err = _("cannot use column reference in FOR PORTION OF expression"); + break; /* * There is intentionally no default: case here, so that the @@ -1764,6 +1767,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_GENERATED_COLUMN: err = _("cannot use subquery in column generation expression"); break; + case EXPR_KIND_UPDATE_PORTION: + err = _("cannot use subquery in FOR PORTION OF expression"); + break; /* * There is intentionally no default: case here, so that the @@ -3039,6 +3045,8 @@ ParseExprKindName(ParseExprKind exprKind) case EXPR_KIND_UPDATE_SOURCE: case EXPR_KIND_UPDATE_TARGET: return "UPDATE"; + case EXPR_KIND_UPDATE_PORTION: + return "FOR PORTION OF"; case EXPR_KIND_GROUP_BY: return "GROUP BY"; case EXPR_KIND_ORDER_BY: diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 542f9167aa..65f1cf4f98 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2655,6 +2655,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_UPDATE_PORTION: + err = _("set-returning functions are not allowed in FOR PORTION OF expressions"); + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c index 9521e81100..503c50e098 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -1591,6 +1591,7 @@ rewriteValuesRTE(Query *parsetree, RangeTblEntry *rte, int rti, /* * Record in target_rte->extraUpdatedCols the indexes of any generated columns * that depend on any columns mentioned in target_rte->updatedCols. + * If the update uses FOR PORTION OF, include the PK range. */ void fill_extraUpdatedCols(RangeTblEntry *target_rte, Relation target_relation) @@ -3122,6 +3123,19 @@ rewriteTargetView(Query *parsetree, Relation view) } } + // TODO: This doesn't look like it's getting used: + if (parsetree->forPortionOf) + { + foreach(lc, parsetree->forPortionOf->rangeSet) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + + if (!tle->resjunk) + modified_cols = bms_add_member(modified_cols, + tle->resno - FirstLowInvalidHeapAttributeNumber); + } + } + auto_update_detail = view_cols_are_auto_updatable(viewquery, modified_cols, NULL, @@ -3764,6 +3778,16 @@ RewriteQuery(Query *parsetree, List *rewrite_events) } else if (event == CMD_UPDATE) { + /* Update FOR PORTION OF column(s) automatically */ + if (parsetree->forPortionOf) + { + ListCell *tl; + foreach(tl, parsetree->forPortionOf->rangeSet) + { + TargetEntry *tle = (TargetEntry *) lfirst(tl); + parsetree->targetList = lappend(parsetree->targetList, tle); + } + } parsetree->targetList = rewriteTargetListIU(parsetree->targetList, parsetree->commandType, diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 1fbc387d47..8f6e80b23d 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -1542,7 +1542,7 @@ ProcessUtilitySlow(ParseState *pstate, true, /* check_rights */ true, /* check_not_in_use */ false, /* skip_build */ - false); /* quiet */ + false); /* quiet */ /* * Add the CREATE INDEX node itself to stash right away; diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile index 41b486bcef..dc741f2fc9 100644 --- a/src/backend/utils/adt/Makefile +++ b/src/backend/utils/adt/Makefile @@ -38,6 +38,7 @@ OBJS = \ float.o \ format_type.o \ formatting.o \ + fp_triggers.o \ genfile.o \ geo_ops.o \ geo_selfuncs.o \ diff --git a/src/backend/utils/adt/fp_triggers.c b/src/backend/utils/adt/fp_triggers.c new file mode 100644 index 0000000000..d00021ace1 --- /dev/null +++ b/src/backend/utils/adt/fp_triggers.c @@ -0,0 +1,574 @@ +/*------------------------------------------------------------------------- + * + * fp_triggers.c + * + * Generic trigger procedures for temporal update and delete commands. + * + * Note about memory management: the private hashtables kept here live + * across query and transaction boundaries, in fact they live as long as + * the backend does. This works because the hashtable structures + * themselves are allocated by dynahash.c in its permanent DynaHashCxt, + * and the SPI plans they point to are saved using SPI_keepplan(). + * There is not currently any provision for throwing away a no-longer-needed + * plan --- consider improving this someday. + * + * + * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group + * + * src/backend/utils/adt/fp_triggers.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/sysattr.h" +#include "access/table.h" +#include "access/tableam.h" +#include "access/xact.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_constraint.h" +#include "catalog/pg_operator.h" +#include "catalog/pg_type.h" +#include "commands/trigger.h" +#include "executor/executor.h" +#include "executor/spi.h" +#include "lib/ilist.h" +#include "miscadmin.h" +#include "parser/parse_coerce.h" +#include "parser/parse_relation.h" +#include "storage/bufmgr.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/fmgroids.h" +#include "utils/guc.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/rel.h" +#include "utils/rls.h" +#include "utils/ruleutils.h" +#include "utils/snapmgr.h" +#include "utils/syscache.h" + +/* + * Local definitions + */ + +/* Need a little more than the possible number of columns in a table */ +#define FP_MAX_ATTS 1650 + +#define FP_INIT_CONSTRAINTHASHSIZE 64 +#define FP_INIT_QUERYHASHSIZE (FP_INIT_CONSTRAINTHASHSIZE * 4) + +#define MAX_QUOTED_NAME_LEN (NAMEDATALEN*2+3) +#define MAX_QUOTED_REL_NAME_LEN (MAX_QUOTED_NAME_LEN*2) + +#define FPAttName(rel, attnum) NameStr(*attnumAttName(rel, attnum)) + + +/* + * FP_QueryKey + * + * The key identifying a prepared SPI plan in our query hashtable + */ +typedef struct FP_QueryKey +{ + Oid relation_id; /* OID of Relation */ +} FP_QueryKey; + +/* + * FP_QueryHashEntry + */ +typedef struct FP_QueryHashEntry +{ + FP_QueryKey key; + SPIPlanPtr plan; +} FP_QueryHashEntry; + +/* + * FP_CompareKey + * + * The key identifying an entry showing how to compare two values + */ +typedef struct FP_CompareKey +{ + Oid eq_opr; /* the equality operator to apply */ + Oid typeid; /* the data type to apply it to */ +} FP_CompareKey; + + +/* + * Local data + */ +static HTAB *fp_query_cache = NULL; + + +/* + * Local function prototypes + */ +static void quoteOneName(char *buffer, const char *name); +static void quoteRelationName(char *buffer, Relation rel); +static void fp_BuildQueryKey(FP_QueryKey *key, + const Relation rel); + +static void fp_InitHashTables(void); +static SPIPlanPtr fp_FetchPreparedPlan(FP_QueryKey *key); +static void fp_HashPreparedPlan(FP_QueryKey *key, SPIPlanPtr plan); + +static SPIPlanPtr fp_PlanInserts(const char *querystr, int nargs, Oid *argtypes, + FP_QueryKey *qkey, Relation query_rel); +static bool fp_PerformInserts(FP_QueryKey *qkey, SPIPlanPtr qplan, + Relation query_rel, + TupleTableSlot *oldslot, Datum targetRange); +static void fp_ExtractValues(TupleTableSlot *slot, + Datum targetRange, + Datum *vals, char *nulls); + + +/* + * FP_insert_leftovers - + * + * Insert leftovers from a temporal UPDATE/DELETE + */ +Datum +FP_insert_leftovers(PG_FUNCTION_ARGS) +{ + TriggerData *trigdata = (TriggerData *) fcinfo->context; + Relation rel = trigdata->tg_relation; + FP_QueryKey qkey; + SPIPlanPtr qplan; + + /* Check that this is a valid trigger call on the right time and event. */ + if (!CALLED_AS_TRIGGER(fcinfo)) + ereport(ERROR, + (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED), + errmsg("function \"%s\" was not called by trigger manager", "FP_insert_leftovers"))); + + if (!TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event) && + !TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)) + ereport(ERROR, + (errcode(ERRCODE_E_R_I_E_TRIGGER_PROTOCOL_VIOLATED), + errmsg("function \"%s\" must be fired for UPDATE or DELETE", "FP_insert_leftovers"))); + + /* Only do something if the statement has FOR PORTION OF */ + if (!trigdata->tg_temporal) + return PointerGetDatum(NULL); + + if (!trigdata->tg_temporal->fp_targetRange) + elog(ERROR, "No target range found for temporal query"); + + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + /* Fetch or prepare a saved plan for the inserts */ + fp_BuildQueryKey(&qkey, rel); + + if ((qplan = fp_FetchPreparedPlan(&qkey)) == NULL) + { + RangeType *targetRange = DatumGetRangeTypeP(trigdata->tg_temporal->fp_targetRange); + char *rangeTypeName = get_typname(RangeTypeGetOid(targetRange)); + StringInfoData querybuf; + int natts = rel->rd_att->natts; + char relname[MAX_QUOTED_REL_NAME_LEN]; + char attname[MAX_QUOTED_NAME_LEN]; + Oid queryoids[FP_MAX_ATTS]; + int rangeAttNum = InvalidAttrNumber; + int periodStartAttNum = InvalidAttrNumber; + int periodEndAttNum = InvalidAttrNumber; + bool usingPeriod; + + /* ---------- + * The query string built is + * INSERT INTO + * (rangeatt, otheratt1, ...) + * SELECT x.r, $1, ... $n + * FROM (VALUES + * (rangetype(lower($x), upper($n+1)) - $n+1), + * (rangetype(lower($n+1), upper($x)) - $n+1) + * ) x (r) + * WHERE x.r <> 'empty' + * The SELECT list "$1, ... $n" includes every attribute except the rangeatt. + * The "$x" is whichever attribute is the range column. + * The $n+1 param has the FOR PORTION OF target range. + * The $1...$n params are the values of the pre-UPDATE/DELETE tuple. + * If there is a PERIOD instead of a range, + * then instead of rangeatt we use startatt and endatt. + * ---------- + */ + initStringInfo(&querybuf); + + usingPeriod = trigdata->tg_temporal->fp_periodStartName != NULL; + quoteRelationName(relname, rel); + appendStringInfo(&querybuf, "INSERT INTO %s (", relname); + if (usingPeriod) + { + quoteOneName(attname, trigdata->tg_temporal->fp_periodStartName); + appendStringInfo(&querybuf, "%s", attname); + quoteOneName(attname, trigdata->tg_temporal->fp_periodEndName); + appendStringInfo(&querybuf, ", %s", attname); + } + else + { + quoteOneName(attname, trigdata->tg_temporal->fp_rangeName); + appendStringInfo(&querybuf, "%s", attname); + } + + /* INSERT into every attribute but the range column */ + for (int i = 0; i < natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i); + const char *colname = NameStr(attr->attname); + if (!usingPeriod && strcmp(colname, trigdata->tg_temporal->fp_rangeName) == 0) + rangeAttNum = i + 1; + else if (usingPeriod && strcmp(colname, trigdata->tg_temporal->fp_periodStartName) == 0) + periodStartAttNum = i + 1; + else if (usingPeriod && strcmp(colname, trigdata->tg_temporal->fp_periodEndName) == 0) + periodEndAttNum = i + 1; + else + { + quoteOneName(attname, colname); + appendStringInfo(&querybuf, ", %s", attname); + } + queryoids[i] = attr->atttypid; + } + queryoids[natts] = trigdata->tg_temporal->fp_rangeType; + if (!usingPeriod && rangeAttNum == InvalidAttrNumber) + elog(ERROR, "range column %s not found", trigdata->tg_temporal->fp_rangeName); + else if (usingPeriod && periodStartAttNum == InvalidAttrNumber) + elog(ERROR, "period start column %s not found", trigdata->tg_temporal->fp_periodStartName); + else if (usingPeriod && periodEndAttNum == InvalidAttrNumber) + elog(ERROR, "period end column %s not found", trigdata->tg_temporal->fp_periodEndName); + + if (!usingPeriod) + appendStringInfo(&querybuf, ") SELECT x.r"); + else + appendStringInfo(&querybuf, ") SELECT lower(x.r), upper(x.r)"); + + /* SELECT all the attributes but the range/start/end columns */ + for (int i = 0; i < natts; i++) + if (!((!usingPeriod && i == rangeAttNum - 1) || + (usingPeriod && i == periodStartAttNum - 1) || + (usingPeriod && i == periodEndAttNum - 1))) + appendStringInfo(&querybuf, ", $%d", i + 1); + + appendStringInfo(&querybuf, " FROM (VALUES"); + // TODO: Why use `- $n+1` instead of setting the bound to the edge of $n+1 directly? + // (where $n+1 is the range build from FOR PORTION OF) + if (!usingPeriod) + { + appendStringInfo(&querybuf, " (%s(lower($%d), upper($%d)) - $%d),", rangeTypeName, rangeAttNum, natts+1, natts+1); + appendStringInfo(&querybuf, " (%s(lower($%d), upper($%d)) - $%d)", rangeTypeName, natts+1, rangeAttNum, natts+1); + } + else + { + appendStringInfo(&querybuf, " (%s($%d, upper($%d)) - $%d),", rangeTypeName, periodStartAttNum, natts+1, natts+1); + appendStringInfo(&querybuf, " (%s(lower($%d), $%d) - $%d)", rangeTypeName, natts+1, periodEndAttNum, natts+1); + } + appendStringInfo(&querybuf, ") x(r) WHERE x.r <> 'empty'"); + + /* Prepare and save the plan */ + qplan = fp_PlanInserts(querybuf.data, natts + (usingPeriod ? 2 : 1), queryoids, &qkey, rel); + } + + /* + * We have a plan now. Run it. + */ + fp_PerformInserts(&qkey, qplan, + rel, + trigdata->tg_trigslot, + trigdata->tg_temporal->fp_targetRange); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish failed"); + + return PointerGetDatum(NULL); +} + + +/* ---------- + * Local functions below + * ---------- + */ + + +/* + * quoteOneName --- safely quote a single SQL name + * + * buffer must be MAX_QUOTED_NAME_LEN long (includes room for \0) + */ +static void +quoteOneName(char *buffer, const char *name) +{ + /* Rather than trying to be smart, just always quote it. */ + *buffer++ = '"'; + while (*name) + { + if (*name == '"') + *buffer++ = '"'; + *buffer++ = *name++; + } + *buffer++ = '"'; + *buffer = '\0'; +} + +/* + * quoteRelationName --- safely quote a fully qualified relation name + * + * buffer must be MAX_QUOTED_REL_NAME_LEN long (includes room for \0) + */ +static void +quoteRelationName(char *buffer, Relation rel) +{ + quoteOneName(buffer, get_namespace_name(RelationGetNamespace(rel))); + buffer += strlen(buffer); + *buffer++ = '.'; + quoteOneName(buffer, RelationGetRelationName(rel)); +} + +/* ---------- + * fp_BuildQueryKey - + * + * Construct a hashtable key for a prepared SPI plan of a temporal leftovers insert + * + * key: output argument, *key is filled in based on the other arguments + * Relation: info from pg_constraint entry + * ---------- + */ +static void +fp_BuildQueryKey(FP_QueryKey *key, const Relation rel) +{ + /* + * We assume struct FP_QueryKey contains no padding bytes, else we'd need + * to use memset to clear them. + */ + key->relation_id = RelationGetRelid(rel); +} + +/* + * Prepare execution plan for a query to insert temporal leftovers + */ +static SPIPlanPtr +fp_PlanInserts(const char *querystr, int nargs, Oid *argtypes, + FP_QueryKey *qkey, Relation query_rel) +{ + SPIPlanPtr qplan; + Oid save_userid; + int save_sec_context; + + /* Switch to proper UID to perform check as */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner, + save_sec_context | SECURITY_LOCAL_USERID_CHANGE | + SECURITY_NOFORCE_RLS); + + /* Create the plan */ + qplan = SPI_prepare(querystr, nargs, argtypes); + + if (qplan == NULL) + elog(ERROR, "SPI_prepare returned %s for %s", SPI_result_code_string(SPI_result), querystr); + + /* Restore UID and security context */ + SetUserIdAndSecContext(save_userid, save_sec_context); + + /* Save the plan */ + SPI_keepplan(qplan); + fp_HashPreparedPlan(qkey, qplan); + + return qplan; +} + +/* + * Perform a query to enforce a temporal PK restriction + */ +static bool +fp_PerformInserts(FP_QueryKey *qkey, SPIPlanPtr qplan, + Relation query_rel, + TupleTableSlot *oldslot, Datum targetRange) +{ + Snapshot test_snapshot; + Snapshot crosscheck_snapshot; + int spi_result; + Oid save_userid; + int save_sec_context; + Datum vals[FP_MAX_ATTS]; + char nulls[FP_MAX_ATTS]; + bool detectNewRows = true; // TODO: need this? + + /* Extract the parameters to be passed into the query */ + fp_ExtractValues(oldslot, targetRange, vals, nulls); + + /* + * In READ COMMITTED mode, we just need to use an up-to-date regular + * snapshot, and we will see all rows that could be interesting. But in + * transaction-snapshot mode, we can't change the transaction snapshot. If + * the caller passes detectNewRows == false then it's okay to do the query + * with the transaction snapshot; otherwise we use a current snapshot, and + * tell the executor to error out if it finds any rows under the current + * snapshot that wouldn't be visible per the transaction snapshot. Note + * that SPI_execute_snapshot will register the snapshots, so we don't need + * to bother here. + */ + if (IsolationUsesXactSnapshot() && detectNewRows) + { + CommandCounterIncrement(); /* be sure all my own work is visible */ + test_snapshot = GetLatestSnapshot(); + crosscheck_snapshot = GetTransactionSnapshot(); + } + else + { + /* the default SPI behavior is okay */ + test_snapshot = InvalidSnapshot; + crosscheck_snapshot = InvalidSnapshot; + } + + /* Switch to proper UID to perform check as */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner, + save_sec_context | SECURITY_LOCAL_USERID_CHANGE | + SECURITY_NOFORCE_RLS); + + /* Finally we can run the query. */ + spi_result = SPI_execute_snapshot(qplan, + vals, nulls, + test_snapshot, crosscheck_snapshot, + false, true, 0); + + /* Restore UID and security context */ + SetUserIdAndSecContext(save_userid, save_sec_context); + + /* Check result */ + if (spi_result < 0) + elog(ERROR, "SPI_execute_snapshot returned %s", SPI_result_code_string(spi_result)); + + if (spi_result != SPI_OK_INSERT) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("temporal leftovers query on \"%s\" gave unexpected result", + RelationGetRelationName(query_rel)), + errhint("This is most likely due to a rule having rewritten the query."))); + + return SPI_processed != 0; +} + +/* + * Extract fields from a tuple into Datum/nulls arrays + */ +static void +fp_ExtractValues(TupleTableSlot *slot, + Datum targetRange, + Datum *vals, char *nulls) +{ + int natts = slot->tts_tupleDescriptor->natts; + bool isnull; + + for (int i = 0; i < natts; i++) + { + vals[i] = slot_getattr(slot, i + 1, &isnull); + nulls[i] = isnull ? 'n' : ' '; + } + vals[natts] = targetRange; + nulls[natts] = false; +} + +/* + * fp_InitHashTables - + * + * Initialize our internal hash tables. + */ +static void +fp_InitHashTables(void) +{ + HASHCTL ctl; + + memset(&ctl, 0, sizeof(ctl)); + ctl.keysize = sizeof(FP_QueryKey); + ctl.entrysize = sizeof(FP_QueryHashEntry); + fp_query_cache = hash_create("FP query cache", + FP_INIT_QUERYHASHSIZE, + &ctl, HASH_ELEM | HASH_BLOBS); +} + + +/* + * fp_FetchPreparedPlan - + * + * Lookup for a query key in our private hash table of prepared + * and saved SPI execution plans. Return the plan if found or NULL. + */ +static SPIPlanPtr +fp_FetchPreparedPlan(FP_QueryKey *key) +{ + FP_QueryHashEntry *entry; + SPIPlanPtr plan; + + /* + * On the first call initialize the hashtable + */ + if (!fp_query_cache) + fp_InitHashTables(); + + /* + * Lookup for the key + */ + entry = (FP_QueryHashEntry *) hash_search(fp_query_cache, + (void *) key, + HASH_FIND, NULL); + if (entry == NULL) + return NULL; + + /* + * Check whether the plan is still valid. If it isn't, we don't want to + * simply rely on plancache.c to regenerate it; rather we should start + * from scratch and rebuild the query text too. This is to cover cases + * such as table/column renames. We depend on the plancache machinery to + * detect possible invalidations, though. + * + * CAUTION: this check is only trustworthy if the caller has already + * locked the rel. + */ + plan = entry->plan; + if (plan && SPI_plan_is_valid(plan)) + return plan; + + /* + * Otherwise we might as well flush the cached plan now, to free a little + * memory space before we make a new one. + */ + entry->plan = NULL; + if (plan) + SPI_freeplan(plan); + + return NULL; +} + + +/* + * fp_HashPreparedPlan - + * + * Add another plan to our private SPI query plan hashtable. + */ +static void +fp_HashPreparedPlan(FP_QueryKey *key, SPIPlanPtr plan) +{ + FP_QueryHashEntry *entry; + bool found; + + /* + * On the first call initialize the hashtable + */ + if (!fp_query_cache) + fp_InitHashTables(); + + /* + * Add the new plan. We might be overwriting an entry previously found + * invalid by fp_FetchPreparedPlan. + */ + entry = (FP_QueryHashEntry *) hash_search(fp_query_cache, + (void *) key, + HASH_ENTER, &found); + Assert(!found || entry->plan == NULL); + entry->plan = plan; +} diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c index 815175a654..5a38178485 100644 --- a/src/backend/utils/adt/rangetypes.c +++ b/src/backend/utils/adt/rangetypes.c @@ -1195,6 +1195,48 @@ range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeT return false; } +/* + * range_leftover_internal - Sets output1 and output2 to the remaining parts of r1 + * after subtracting r2, or if nothing is left then to the empty range. + * output1 will always be "before" r2 and output2 "after". + */ +void +range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1, + const RangeType *r2, RangeType **output1, RangeType **output2) +{ + RangeBound lower1, + lower2; + RangeBound upper1, + upper2; + bool empty1, + empty2; + + range_deserialize(typcache, r1, &lower1, &upper1, &empty1); + range_deserialize(typcache, r2, &lower2, &upper2, &empty2); + + if (range_cmp_bounds(typcache, &lower1, &lower2) < 0) + { + lower2.inclusive = !lower2.inclusive; + lower2.lower = false; + *output1 = make_range(typcache, &lower1, &lower2, false); + } + else + { + *output1 = make_empty_range(typcache); + } + + if (range_cmp_bounds(typcache, &upper1, &upper2) > 0) + { + upper2.inclusive = !upper2.inclusive; + upper2.lower = true; + *output2 = make_range(typcache, &upper2, &upper1, false); + } + else + { + *output2 = make_empty_range(typcache); + } +} + /* range -> range aggregate functions */ Datum diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index d068d6532e..5537740256 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3883,6 +3883,12 @@ prorettype => 'trigger', proargtypes => '', prosrc => 'RI_FKey_noaction_upd' }, +# Temporal leftovers triggers +{ oid => '8157', descr => 'temporal leftovers trigger function', + proname => 'FP_insert_leftovers', provolatile => 'v', + prorettype => 'trigger', proargtypes => '', + prosrc => 'FP_insert_leftovers' }, + { oid => '1666', proname => 'varbiteq', proleakproof => 't', prorettype => 'bool', proargtypes => 'varbit varbit', prosrc => 'biteq' }, diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index 89239205d2..8b35a427ae 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -41,6 +41,8 @@ extern void ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, L extern void AlterTableInternal(Oid relid, List *cmds, bool recurse, struct AlterTableUtilityContext *context); +extern void CreateTemporalPrimaryKeyTriggers(Relation rel, Oid constraintOid, Oid indexOid); + extern Oid AlterTableMoveAll(AlterTableMoveAllStmt *stmt); extern ObjectAddress AlterTableNamespace(AlterObjectSchemaStmt *stmt, diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index 9ef7f6d768..e6792b7281 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -40,6 +40,7 @@ typedef struct TriggerData Tuplestorestate *tg_oldtable; Tuplestorestate *tg_newtable; const Bitmapset *tg_updatedcols; + ForPortionOfState *tg_temporal; } TriggerData; /* diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 62cb6f5054..fafa03c425 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -26,12 +26,14 @@ #include "storage/condition_variable.h" #include "utils/hsearch.h" #include "utils/queryenvironment.h" +#include "utils/rangetypes.h" #include "utils/reltrigger.h" #include "utils/sharedtuplestore.h" #include "utils/snapshot.h" #include "utils/sortsupport.h" #include "utils/tuplesort.h" #include "utils/tuplestore.h" +#include "utils/typcache.h" struct PlanState; /* forward references in this file */ struct ParallelHashJoinState; @@ -388,6 +390,23 @@ typedef struct OnConflictSetState ExprState *oc_WhereClause; /* state for the WHERE clause */ } OnConflictSetState; +/* + * ForPortionOfState + * + * Executor state of a FOR PORTION OF operation. + */ +typedef struct ForPortionOfState +{ + NodeTag type; + + char *fp_rangeName; /* the column/PERIOD named in FOR PORTION OF */ + Oid fp_rangeType; /* the type of the FOR PORTION OF expression */ + bool fp_hasPeriod; /* true iff this is a PERIOD not a range */ + char *fp_periodStartName; /* the PERIOD's start column */ + char *fp_periodEndName; /* the PERIOD's end column */ + Datum fp_targetRange; /* the range from FOR PORTION OF */ +} ForPortionOfState; + /* * ResultRelInfo * @@ -499,6 +518,9 @@ typedef struct ResultRelInfo /* ON CONFLICT evaluation state */ OnConflictSetState *ri_onConflict; + /* FOR PORTION OF evaluation state */ + ForPortionOfState *ri_forPortionOf; + /* partition check expression state (NULL if not set up yet) */ ExprState *ri_PartitionCheckExpr; diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 8cb6def639..84546ac9c6 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -35,6 +35,7 @@ typedef enum NodeTag T_ProjectionInfo, T_JunkFilter, T_OnConflictSetState, + T_ForPortionOfState, T_ResultRelInfo, T_EState, T_TupleTableSlot, @@ -200,6 +201,7 @@ typedef enum NodeTag T_JoinExpr, T_FromExpr, T_OnConflictExpr, + T_ForPortionOfExpr, T_IntoClause, /* @@ -479,6 +481,7 @@ typedef enum NodeTag T_OnConflictClause, T_CTESearchClause, T_CTECycleClause, + T_ForPortionOfClause, T_CommonTableExpr, T_RoleSpec, T_TriggerTransition, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 647dfdc144..d2a59ca755 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -130,6 +130,8 @@ typedef struct Query int resultRelation; /* rtable index of target relation for * INSERT/UPDATE/DELETE; 0 for SELECT */ + ForPortionOfExpr *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */ + bool hasAggs; /* has aggregates in tlist or havingQual */ bool hasWindowFuncs; /* has window functions in tlist */ bool hasTargetSRFs; /* has set-returning functions in tlist */ @@ -1423,6 +1425,19 @@ typedef struct RowMarkClause bool pushedDown; /* pushed down from higher query level? */ } RowMarkClause; +/* + * ForPortionOfClause + * representation of FOR PORTION OF FROM TO + */ +typedef struct ForPortionOfClause +{ + NodeTag type; + char *range_name; + int range_name_location; + Node *target_start; + Node *target_end; +} ForPortionOfClause; + /* * WithClause - * representation of WITH clause @@ -1604,12 +1619,13 @@ typedef struct InsertStmt */ typedef struct DeleteStmt { - NodeTag type; - RangeVar *relation; /* relation to delete from */ - List *usingClause; /* optional using clause for more tables */ - Node *whereClause; /* qualifications */ - List *returningList; /* list of expressions to return */ - WithClause *withClause; /* WITH clause */ + NodeTag type; + RangeVar *relation; /* relation to delete from */ + ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */ + List *usingClause; /* optional using clause for more tables */ + Node *whereClause; /* qualifications */ + List *returningList; /* list of expressions to return */ + WithClause *withClause; /* WITH clause */ } DeleteStmt; /* ---------------------- @@ -1618,13 +1634,14 @@ typedef struct DeleteStmt */ typedef struct UpdateStmt { - NodeTag type; - RangeVar *relation; /* relation to update */ - List *targetList; /* the target list (of ResTarget) */ - Node *whereClause; /* qualifications */ - List *fromClause; /* optional from clause for more tables */ - List *returningList; /* list of expressions to return */ - WithClause *withClause; /* WITH clause */ + NodeTag type; + RangeVar *relation; /* relation to update */ + ForPortionOfClause *forPortionOf; /* FOR PORTION OF clause */ + List *targetList; /* the target list (of ResTarget) */ + Node *whereClause; /* qualifications */ + List *fromClause; /* optional from clause for more tables */ + List *returningList; /* list of expressions to return */ + WithClause *withClause; /* WITH clause */ } UpdateStmt; /* ---------------------- diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 186e89905b..e9b7121bad 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1893,6 +1893,7 @@ typedef struct ModifyTablePath List *returningLists; /* per-target-table RETURNING tlists */ List *rowMarks; /* PlanRowMarks (non-locking only) */ OnConflictExpr *onconflict; /* ON CONFLICT clause, or NULL */ + ForPortionOfExpr *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */ int epqParam; /* ID of Param for EvalPlanQual re-eval */ } ModifyTablePath; diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 01a246d50e..50261b641a 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -231,6 +231,8 @@ typedef struct ModifyTable List *rowMarks; /* PlanRowMarks (non-locking only) */ int epqParam; /* ID of Param for EvalPlanQual re-eval */ OnConflictAction onConflictAction; /* ON CONFLICT action */ + // TODO: Instead of re-using Expr here, break it into pieces like onConflict{Action,Set,Where}? + Node *forPortionOf; /* FOR PORTION OF clause for UPDATE/DELETE */ List *arbiterIndexes; /* List of ON CONFLICT arbiter index OIDs */ List *onConflictSet; /* INSERT ON CONFLICT DO UPDATE targetlist */ List *onConflictCols; /* target column numbers for onConflictSet */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 433437643e..007e9ef458 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1592,4 +1592,31 @@ typedef struct OnConflictExpr List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */ } OnConflictExpr; +/*---------- + * ForPortionOfExpr - represents a FOR PORTION OF ... expression + * + * TODO: more notes as needed + *---------- + */ +typedef struct ForPortionOfExpr +{ + NodeTag type; + int range_attno; /* Range column number */ + int start_attno; /* PERIOD start column number */ + int end_attno; /* PERIOD end column number */ + char *range_name; /* Range name */ + char *period_start_name; /* PERIOD start column name */ + char *period_end_name; /* PERIOD end column name */ + Expr *range; /* Range column or expression */ + // TODO: I do really use all these fields?: + Node *startCol; /* Start column if using a PERIOD */ + Node *endCol; /* End column if using a PERIOD */ + Node *targetStart; /* Same type as the range's elements */ + Node *targetEnd; /* Same type as the range's elements */ + Node *targetRange; /* A range from targetStart to targetEnd */ + Oid rangeType; /* type of targetRange */ + Node *overlapsExpr; /* range && targetRange */ + List *rangeSet; /* List of TargetEntrys to set the time column(s) */ +} ForPortionOfExpr; + #endif /* PRIMNODES_H */ diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index f704d39980..192e2a192b 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -275,7 +275,7 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root, List *updateColnosLists, List *withCheckOptionLists, List *returningLists, List *rowMarks, OnConflictExpr *onconflict, - int epqParam); + ForPortionOfExpr *forPortionOf, int epqParam); extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, Node *limitOffset, Node *limitCount, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 43fc561075..7545038a9a 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -316,6 +316,7 @@ PG_KEYWORD("period", PERIOD, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("portion", PORTION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD, AS_LABEL) diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index ee179082ce..8f1f629c86 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -55,6 +55,7 @@ typedef enum ParseExprKind EXPR_KIND_INSERT_TARGET, /* INSERT target list item */ EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */ EXPR_KIND_UPDATE_TARGET, /* UPDATE assignment target item */ + EXPR_KIND_UPDATE_PORTION, /* UPDATE FOR PORTION OF item */ EXPR_KIND_GROUP_BY, /* GROUP BY */ EXPR_KIND_ORDER_BY, /* ORDER BY */ EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */ diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h index 04c302c619..d8c1f1656a 100644 --- a/src/include/utils/rangetypes.h +++ b/src/include/utils/rangetypes.h @@ -147,5 +147,8 @@ extern RangeType *make_empty_range(TypeCacheEntry *typcache); extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeType *r2, RangeType **output1, RangeType **output2); +extern void range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1, + const RangeType *r2, RangeType **output1, + RangeType **output2); #endif /* RANGETYPES_H */ diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out new file mode 100644 index 0000000000..c70baad685 --- /dev/null +++ b/src/test/regress/expected/for_portion_of.out @@ -0,0 +1,674 @@ +-- Tests for UPDATE/DELETE FOR PORTION OF +-- Fails on tables without a temporal PK: +CREATE TABLE for_portion_of_test ( + id int4range PRIMARY KEY, + valid_at tsrange NOT NULL, + name text NOT NULL +); +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE +SET name = 'foo'; +ERROR: relation "for_portion_of_test" does not have a temporal primary key +LINE 2: FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE + ^ +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE; +ERROR: relation "for_portion_of_test" does not have a temporal primary key +LINE 2: FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE; + ^ +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_at tsrange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test +VALUES +('[1,2)', '[2018-01-02,2018-02-03)', 'one'), +('[1,2)', '[2018-02-03,2018-03-03)', 'one'), +('[1,2)', '[2018-03-03,2018-04-04)', 'one'), +('[2,3)', '[2018-01-01,2018-01-05)', 'two'), +('[3,4)', '[2018-01-01,)', 'three'), +('[4,5)', '(,2018-04-01)', 'four'), +('[5,6)', '(,)', 'five') +; +-- +-- UPDATE tests +-- +-- Setting with a missing column fails +UPDATE for_portion_of_test +FOR PORTION OF invalid_at FROM '2018-06-01' TO MAXVALUE +SET name = 'foo' +WHERE id = '[5,6)'; +ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist +LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO MAXVALUE + ^ +-- Setting the range fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET valid_at = '[1990-01-01,1999-01-01)' +WHERE id = '[5,6)'; +ERROR: multiple assignments to same column "valid_at" +-- Setting with timestamps reversed fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' +SET name = 'three^1' +WHERE id = '[3,4)'; +ERROR: range lower bound must be less than or equal to range upper bound +-- Setting with MAXVALUE lower bound fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MAXVALUE TO NULL +SET name = 'nope' +WHERE id = '[3,4)'; +ERROR: MAXVALUE can only be used for the upper bound +-- Setting with MINVALUE upper bound fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO MINVALUE +SET name = 'nope' +WHERE id = '[3,4)'; +ERROR: MINVALUE can only be used for the lower bound +-- Setting with a subquery fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' +SET name = 'nope' +WHERE id = '[3,4)'; +ERROR: cannot use subquery in FOR PORTION OF expression +LINE 2: FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-... + ^ +-- Setting with a column fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM lower(valid_at) TO MAXVALUE +SET name = 'nope' +WHERE id = '[3,4)'; +ERROR: cannot use column reference in FOR PORTION OF expression +LINE 2: FOR PORTION OF valid_at FROM lower(valid_at) TO MAXVALUE + ^ +-- Setting with timestamps equal does nothing +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' +SET name = 'three^0' +WHERE id = '[3,4)'; +-- Updating a finite/open portion with a finite/open target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET name = 'three^1' +WHERE id = '[3,4)'; +-- Updating a finite/open portion with an open/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO '2018-03-01' +SET name = 'three^2' +WHERE id = '[3,4)'; +-- Updating an open/finite portion with an open/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO '2018-02-01' +SET name = 'four^1' +WHERE id = '[4,5)'; +-- Updating an open/finite portion with a finite/open target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO MAXVALUE +SET name = 'four^2' +WHERE id = '[4,5)'; +-- Updating a finite/finite portion with an exact fit +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01' +SET name = 'four^3' +WHERE id = '[4,5)'; +-- Updating an enclosed span +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO MAXVALUE +SET name = 'two^2' +WHERE id = '[2,3)'; +-- Updating an open/open portion with a finite/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' +SET name = 'five^2' +WHERE id = '[5,6)'; +-- Updating an enclosed span with separate protruding spans +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01' +SET name = 'five^3' +WHERE id = '[5,6)'; +-- Updating multiple enclosed spans +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO NULL +SET name = 'one^2' +WHERE id = '[1,2)'; +-- Updating with a shift/reduce conflict +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE + TO '2019-01-01' +SET name = 'one^3' +WHERE id = '[1,2)'; +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR + TO '2019-01-01' +SET name = 'one^4' +WHERE id = '[1,2)'; +ERROR: syntax error at or near "'2019-01-01'" +LINE 4: TO '2019-01-01' + ^ +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR) + TO '2019-01-01' +SET name = 'one^4' +WHERE id = '[1,2)'; +-- Updating the non-range part of the PK: +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-02-15' TO NULL +SET id = '[6,7)' +WHERE id = '[1,2)'; +-- UPDATE with no WHERE clause +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2030-01-01' TO NULL +SET name = name || '*'; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+---------------------------------------------------------+---------- + [1,2) | ["Tue Jan 02 00:00:00 2018","Sat Feb 03 00:00:00 2018") | one^2 + [1,2) | ["Sat Feb 03 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2 + [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2 + [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2 + [3,4) | ["Thu Mar 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | three + [3,4) | ["Fri Jun 01 00:00:00 2018","Tue Jan 01 00:00:00 2030") | three^1 + [3,4) | ["Tue Jan 01 00:00:00 2030",) | three^1* + [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1 + [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3 + [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2 + [5,6) | (,"Sun Jan 01 00:00:00 2017") | five + [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3 + [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | five^3 + [5,6) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | five^3 + [5,6) | ["Wed Jan 01 00:00:00 2020","Tue Jan 01 00:00:00 2030") | five + [5,6) | ["Tue Jan 01 00:00:00 2030",) | five* + [6,7) | ["Thu Feb 15 00:00:00 2018","Thu Mar 01 08:01:00 2018") | one^2 + [6,7) | ["Thu Mar 01 08:01:00 2018","Thu Mar 01 10:00:00 2018") | one^3 + [6,7) | ["Thu Mar 01 10:00:00 2018","Sat Mar 03 00:00:00 2018") | one^4 + [6,7) | ["Sat Mar 03 00:00:00 2018","Wed Apr 04 00:00:00 2018") | one^4 +(20 rows) + +-- +-- DELETE tests +-- +-- Deleting with a missing column fails +DELETE FROM for_portion_of_test +FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL +WHERE id = '[5,6)'; +ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist +LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + ^ +-- Deleting with timestamps reversed fails +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' +WHERE id = '[3,4)'; +ERROR: range lower bound must be less than or equal to range upper bound +-- Deleting with timestamps equal does nothing +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' +WHERE id = '[3,4)'; +-- Deleting with a closed/closed target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01' +WHERE id = '[5,6)'; +-- Deleting with a closed/open target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO NULL +WHERE id = '[3,4)'; +-- Deleting with an open/closed target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO '2018-02-08' +WHERE id = '[1,2)'; +-- Deleting with an open/open target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO NULL +WHERE id = '[6,7)'; +-- DELETE with no WHERE clause +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2025-01-01' TO NULL; +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+---------------------------------------------------------+--------- + [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2 + [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2 + [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2 + [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three + [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1 + [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3 + [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2 + [5,6) | (,"Sun Jan 01 00:00:00 2017") | five + [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3 + [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3 + [5,6) | ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025") | five +(11 rows) + +-- UPDATE ... RETURNING returns only the updated values (not the inserted side values) +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15' +SET name = 'three^3' +WHERE id = '[3,4)' +RETURNING *; + id | valid_at | name +-------+---------------------------------------------------------+--------- + [3,4) | ["Thu Feb 01 00:00:00 2018","Thu Feb 15 00:00:00 2018") | three^3 +(1 row) + +-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows +CREATE FUNCTION for_portion_of_trigger() +RETURNS trigger +AS +$$ +BEGIN + RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at; + IF TG_OP = 'DELETE' THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; +END; +$$ +LANGUAGE plpgsql; +CREATE TRIGGER trg_for_portion_of_before_insert + BEFORE INSERT ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_after_insert + AFTER INSERT ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_before_update + BEFORE UPDATE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_after_update + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_before_delete + BEFORE DELETE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_after_delete + AFTER DELETE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01' +SET name = 'five^4' +WHERE id = '[5,6)'; +NOTICE: BEFORE UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025") +NOTICE: BEFORE INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of +NOTICE: BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of +NOTICE: AFTER INSERT ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") of +NOTICE: AFTER INSERT ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") of +NOTICE: AFTER UPDATE ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") of ["Mon Jun 01 00:00:00 2020","Wed Jan 01 00:00:00 2025") +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01' +WHERE id = '[5,6)'; +NOTICE: BEFORE DELETE of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") +NOTICE: BEFORE INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of +NOTICE: BEFORE INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of +NOTICE: AFTER INSERT ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") of +NOTICE: AFTER INSERT ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") of +NOTICE: AFTER DELETE of ["Sat Jan 01 00:00:00 2022","Wed Jan 01 00:00:00 2025") +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + id | valid_at | name +-------+---------------------------------------------------------+--------- + [1,2) | ["Thu Feb 08 00:00:00 2018","Thu Feb 15 00:00:00 2018") | one^2 + [2,3) | ["Mon Jan 01 00:00:00 2018","Fri Jan 05 00:00:00 2018") | two^2 + [3,4) | ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018") | three^2 + [3,4) | ["Thu Feb 01 00:00:00 2018","Thu Feb 15 00:00:00 2018") | three^3 + [3,4) | ["Thu Feb 15 00:00:00 2018","Thu Mar 01 00:00:00 2018") | three^2 + [3,4) | ["Thu Mar 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | three + [4,5) | (,"Sun Jan 01 00:00:00 2017") | four^1 + [4,5) | ["Sun Jan 01 00:00:00 2017","Thu Feb 01 00:00:00 2018") | four^3 + [4,5) | ["Thu Feb 01 00:00:00 2018","Sun Apr 01 00:00:00 2018") | four^2 + [5,6) | (,"Sun Jan 01 00:00:00 2017") | five + [5,6) | ["Sun Jan 01 00:00:00 2017","Mon Jan 01 00:00:00 2018") | five^3 + [5,6) | ["Mon Jan 01 00:00:00 2018","Fri Jun 01 00:00:00 2018") | five^3 + [5,6) | ["Mon Jun 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | five + [5,6) | ["Fri Jan 01 00:00:00 2021","Sat Jan 01 00:00:00 2022") | five^4 + [5,6) | ["Sat Jan 01 00:00:00 2022","Sun Jan 01 00:00:00 2023") | five + [5,6) | ["Mon Jan 01 00:00:00 2024","Wed Jan 01 00:00:00 2025") | five +(16 rows) + +-- +-- Now re-run the same tests but with a PERIOD instead of a range: +-- +DROP TABLE for_portion_of_test; +-- Fails on tables without a temporal PK: +CREATE TABLE for_portion_of_test ( + id int4range PRIMARY KEY, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + name text NOT NULL +); +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE +SET name = 'foo'; +ERROR: relation "for_portion_of_test" does not have a temporal primary key +LINE 2: FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE + ^ +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE; +ERROR: relation "for_portion_of_test" does not have a temporal primary key +LINE 2: FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE; + ^ +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test +VALUES +('[1,2)', '2018-01-02', '2018-02-03', 'one'), +('[1,2)', '2018-02-03', '2018-03-03', 'one'), +('[1,2)', '2018-03-03', '2018-04-04', 'one'), +('[2,3)', '2018-01-01', '2018-01-05', 'two'), +('[3,4)', '2018-01-01', null, 'three'), +('[4,5)', null, '2018-04-01', 'four'), +('[5,6)', null, null, 'five') +; +-- +-- UPDATE tests +-- +-- Setting with a missing column fails +UPDATE for_portion_of_test +FOR PORTION OF invalid_at FROM '2018-06-01' TO MAXVALUE +SET name = 'foo' +WHERE id = '[5,6)'; +ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist +LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO MAXVALUE + ^ +-- Setting the start column fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET valid_from = '1990-01-01' +WHERE id = '[5,6)'; +ERROR: multiple assignments to same column "valid_from" +-- Setting the end column fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET valid_til = '1999-01-01' +WHERE id = '[5,6)'; +ERROR: multiple assignments to same column "valid_til" +-- Setting with timestamps reversed fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' +SET name = 'three^1' +WHERE id = '[3,4)'; +ERROR: range lower bound must be less than or equal to range upper bound +-- Setting with a subquery fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' +SET name = 'nope' +WHERE id = '[3,4)'; +ERROR: cannot use subquery in FOR PORTION OF expression +LINE 2: FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-... + ^ +-- Setting with a column fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM valid_from TO MAXVALUE +SET name = 'nope' +WHERE id = '[3,4)'; +ERROR: cannot use column reference in FOR PORTION OF expression +LINE 2: FOR PORTION OF valid_at FROM valid_from TO MAXVALUE + ^ +-- Setting with timestamps equal does nothing +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' +SET name = 'three^0' +WHERE id = '[3,4)'; +-- Updating a finite/open portion with a finite/open target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET name = 'three^1' +WHERE id = '[3,4)'; +-- Updating a finite/open portion with an open/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO '2018-03-01' +SET name = 'three^2' +WHERE id = '[3,4)'; +-- Updating an open/finite portion with an open/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO '2018-02-01' +SET name = 'four^1' +WHERE id = '[4,5)'; +-- Updating an open/finite portion with a finite/open target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO MAXVALUE +SET name = 'four^2' +WHERE id = '[4,5)'; +-- Updating a finite/finite portion with an exact fit +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01' +SET name = 'four^3' +WHERE id = '[4,5)'; +-- Updating an enclosed span +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO MAXVALUE +SET name = 'two^2' +WHERE id = '[2,3)'; +-- Updating an open/open portion with a finite/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' +SET name = 'five^2' +WHERE id = '[5,6)'; +-- Updating an enclosed span with separate protruding spans +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01' +SET name = 'five^3' +WHERE id = '[5,6)'; +-- Updating multiple enclosed spans +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO NULL +SET name = 'one^2' +WHERE id = '[1,2)'; +-- Updating with a shift/reduce conflict +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE + TO '2019-01-01' +SET name = 'one^3' +WHERE id = '[1,2)'; +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR + TO '2019-01-01' +SET name = 'one^4' +WHERE id = '[1,2)'; +ERROR: syntax error at or near "'2019-01-01'" +LINE 4: TO '2019-01-01' + ^ +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR) + TO '2019-01-01' +SET name = 'one^4' +WHERE id = '[1,2)'; +-- Updating the non-range part of the PK: +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-02-15' TO NULL +SET id = '[6,7)' +WHERE id = '[1,2)'; +-- UPDATE with no WHERE clause +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2030-01-01' TO NULL +SET name = name || '*'; +-- TODO: UPDATE with generated columns too +SELECT * FROM for_portion_of_test ORDER BY id, valid_from NULLS FIRST, valid_til; + id | valid_from | valid_til | name +-------+--------------------------+--------------------------+---------- + [1,2) | Tue Jan 02 00:00:00 2018 | Sat Feb 03 00:00:00 2018 | one^2 + [1,2) | Sat Feb 03 00:00:00 2018 | Thu Feb 15 00:00:00 2018 | one^2 + [2,3) | Mon Jan 01 00:00:00 2018 | Fri Jan 05 00:00:00 2018 | two^2 + [3,4) | Mon Jan 01 00:00:00 2018 | Thu Mar 01 00:00:00 2018 | three^2 + [3,4) | Thu Mar 01 00:00:00 2018 | Fri Jun 01 00:00:00 2018 | three + [3,4) | Fri Jun 01 00:00:00 2018 | Tue Jan 01 00:00:00 2030 | three^1 + [3,4) | Tue Jan 01 00:00:00 2030 | | three^1* + [4,5) | | Sun Jan 01 00:00:00 2017 | four^1 + [4,5) | Sun Jan 01 00:00:00 2017 | Thu Feb 01 00:00:00 2018 | four^3 + [4,5) | Thu Feb 01 00:00:00 2018 | Sun Apr 01 00:00:00 2018 | four^2 + [5,6) | | Sun Jan 01 00:00:00 2017 | five + [5,6) | Sun Jan 01 00:00:00 2017 | Mon Jan 01 00:00:00 2018 | five^3 + [5,6) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | five^3 + [5,6) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | five^3 + [5,6) | Wed Jan 01 00:00:00 2020 | Tue Jan 01 00:00:00 2030 | five + [5,6) | Tue Jan 01 00:00:00 2030 | | five* + [6,7) | Thu Feb 15 00:00:00 2018 | Thu Mar 01 08:01:00 2018 | one^2 + [6,7) | Thu Mar 01 08:01:00 2018 | Thu Mar 01 10:00:00 2018 | one^3 + [6,7) | Thu Mar 01 10:00:00 2018 | Sat Mar 03 00:00:00 2018 | one^4 + [6,7) | Sat Mar 03 00:00:00 2018 | Wed Apr 04 00:00:00 2018 | one^4 +(20 rows) + +-- +-- DELETE tests +-- +-- Deleting with a missing column fails +DELETE FROM for_portion_of_test +FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL +WHERE id = '[5,6)'; +ERROR: column or period "invalid_at" of relation "for_portion_of_test" does not exist +LINE 2: FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL + ^ +-- Deleting with timestamps reversed fails +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' +WHERE id = '[3,4)'; +ERROR: range lower bound must be less than or equal to range upper bound +-- Deleting with timestamps equal does nothing +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' +WHERE id = '[3,4)'; +-- Deleting with a closed/closed target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01' +WHERE id = '[5,6)'; +-- Deleting with a closed/open target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO NULL +WHERE id = '[3,4)'; +-- Deleting with an open/closed target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO '2018-02-08' +WHERE id = '[1,2)'; +-- Deleting with an open/open target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO NULL +WHERE id = '[6,7)'; +-- DELETE with no WHERE clause +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2025-01-01' TO NULL; +SELECT * FROM for_portion_of_test ORDER BY id, valid_from NULLS FIRST, valid_til; + id | valid_from | valid_til | name +-------+--------------------------+--------------------------+--------- + [1,2) | Thu Feb 08 00:00:00 2018 | Thu Feb 15 00:00:00 2018 | one^2 + [2,3) | Mon Jan 01 00:00:00 2018 | Fri Jan 05 00:00:00 2018 | two^2 + [3,4) | Mon Jan 01 00:00:00 2018 | Thu Mar 01 00:00:00 2018 | three^2 + [3,4) | Thu Mar 01 00:00:00 2018 | Sun Apr 01 00:00:00 2018 | three + [4,5) | | Sun Jan 01 00:00:00 2017 | four^1 + [4,5) | Sun Jan 01 00:00:00 2017 | Thu Feb 01 00:00:00 2018 | four^3 + [4,5) | Thu Feb 01 00:00:00 2018 | Sun Apr 01 00:00:00 2018 | four^2 + [5,6) | | Sun Jan 01 00:00:00 2017 | five + [5,6) | Sun Jan 01 00:00:00 2017 | Mon Jan 01 00:00:00 2018 | five^3 + [5,6) | Mon Jan 01 00:00:00 2018 | Fri Jun 01 00:00:00 2018 | five^3 + [5,6) | Mon Jun 01 00:00:00 2020 | Wed Jan 01 00:00:00 2025 | five +(11 rows) + +-- UPDATE ... RETURNING returns only the updated values (not the inserted side values) +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15' +SET name = 'three^3' +WHERE id = '[3,4)' +RETURNING *; + id | valid_from | valid_til | name +-------+--------------------------+--------------------------+--------- + [3,4) | Thu Feb 01 00:00:00 2018 | Thu Feb 15 00:00:00 2018 | three^3 +(1 row) + +-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows +CREATE FUNCTION for_portion_of_trigger2() +RETURNS trigger +AS +$$ +BEGIN + RAISE NOTICE '% % [%,%) of [%,%)', TG_WHEN, TG_OP, NEW.valid_from, NEW.valid_til, OLD.valid_from, OLD.valid_til; + IF TG_OP = 'DELETE' THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; +END; +$$ +LANGUAGE plpgsql; +CREATE TRIGGER trg_for_portion_of_before_insert + BEFORE INSERT ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_after_insert + AFTER INSERT ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_before_update + BEFORE UPDATE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_after_update + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_before_delete + BEFORE DELETE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_after_delete + AFTER DELETE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01' +SET name = 'five^4' +WHERE id = '[5,6)'; +NOTICE: BEFORE UPDATE [Fri Jan 01 00:00:00 2021,Sat Jan 01 00:00:00 2022) of [Mon Jun 01 00:00:00 2020,Wed Jan 01 00:00:00 2025) +NOTICE: BEFORE INSERT [Mon Jun 01 00:00:00 2020,Fri Jan 01 00:00:00 2021) of [,) +NOTICE: BEFORE INSERT [Sat Jan 01 00:00:00 2022,Wed Jan 01 00:00:00 2025) of [,) +NOTICE: AFTER INSERT [Mon Jun 01 00:00:00 2020,Fri Jan 01 00:00:00 2021) of [,) +NOTICE: AFTER INSERT [Sat Jan 01 00:00:00 2022,Wed Jan 01 00:00:00 2025) of [,) +NOTICE: AFTER UPDATE [Fri Jan 01 00:00:00 2021,Sat Jan 01 00:00:00 2022) of [Mon Jun 01 00:00:00 2020,Wed Jan 01 00:00:00 2025) +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01' +WHERE id = '[5,6)'; +NOTICE: BEFORE DELETE [,) of [Sat Jan 01 00:00:00 2022,Wed Jan 01 00:00:00 2025) +NOTICE: BEFORE INSERT [Sat Jan 01 00:00:00 2022,Sun Jan 01 00:00:00 2023) of [,) +NOTICE: BEFORE INSERT [Mon Jan 01 00:00:00 2024,Wed Jan 01 00:00:00 2025) of [,) +NOTICE: AFTER INSERT [Sat Jan 01 00:00:00 2022,Sun Jan 01 00:00:00 2023) of [,) +NOTICE: AFTER INSERT [Mon Jan 01 00:00:00 2024,Wed Jan 01 00:00:00 2025) of [,) +NOTICE: AFTER DELETE [,) of [Sat Jan 01 00:00:00 2022,Wed Jan 01 00:00:00 2025) +SELECT * FROM for_portion_of_test ORDER BY id, valid_from NULLS FIRST, valid_til; + id | valid_from | valid_til | name +-------+--------------------------+--------------------------+--------- + [1,2) | Thu Feb 08 00:00:00 2018 | Thu Feb 15 00:00:00 2018 | one^2 + [2,3) | Mon Jan 01 00:00:00 2018 | Fri Jan 05 00:00:00 2018 | two^2 + [3,4) | Mon Jan 01 00:00:00 2018 | Thu Feb 01 00:00:00 2018 | three^2 + [3,4) | Thu Feb 01 00:00:00 2018 | Thu Feb 15 00:00:00 2018 | three^3 + [3,4) | Thu Feb 15 00:00:00 2018 | Thu Mar 01 00:00:00 2018 | three^2 + [3,4) | Thu Mar 01 00:00:00 2018 | Sun Apr 01 00:00:00 2018 | three + [4,5) | | Sun Jan 01 00:00:00 2017 | four^1 + [4,5) | Sun Jan 01 00:00:00 2017 | Thu Feb 01 00:00:00 2018 | four^3 + [4,5) | Thu Feb 01 00:00:00 2018 | Sun Apr 01 00:00:00 2018 | four^2 + [5,6) | | Sun Jan 01 00:00:00 2017 | five + [5,6) | Sun Jan 01 00:00:00 2017 | Mon Jan 01 00:00:00 2018 | five^3 + [5,6) | Mon Jan 01 00:00:00 2018 | Fri Jun 01 00:00:00 2018 | five^3 + [5,6) | Mon Jun 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | five + [5,6) | Fri Jan 01 00:00:00 2021 | Sat Jan 01 00:00:00 2022 | five^4 + [5,6) | Sat Jan 01 00:00:00 2022 | Sun Jan 01 00:00:00 2023 | five + [5,6) | Mon Jan 01 00:00:00 2024 | Wed Jan 01 00:00:00 2025 | five +(16 rows) + diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index 6836f41f09..539e9688eb 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -48,6 +48,7 @@ f_star|f fast_emp4000|t float4_tbl|f float8_tbl|f +for_portion_of_test|t func_index_heap|t hash_f8_heap|t hash_i4_heap|t diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 088387281a..5e78aaf854 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -58,7 +58,7 @@ test: create_index create_index_spgist create_view index_including index_includi # ---------- # Another group of parallel tests # ---------- -test: create_aggregate create_function_3 create_cast constraints triggers select inherit typed_table periods vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse +test: create_aggregate create_function_3 create_cast constraints triggers select inherit typed_table periods vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse for_portion_of # ---------- # sanity_check does a vacuum, affecting the sort order of SELECT * diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql new file mode 100644 index 0000000000..3b5755ba00 --- /dev/null +++ b/src/test/regress/sql/for_portion_of.sql @@ -0,0 +1,562 @@ +-- Tests for UPDATE/DELETE FOR PORTION OF + +-- Fails on tables without a temporal PK: +CREATE TABLE for_portion_of_test ( + id int4range PRIMARY KEY, + valid_at tsrange NOT NULL, + name text NOT NULL +); + +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE +SET name = 'foo'; + +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE; + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_at tsrange NOT NULL, + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test +VALUES +('[1,2)', '[2018-01-02,2018-02-03)', 'one'), +('[1,2)', '[2018-02-03,2018-03-03)', 'one'), +('[1,2)', '[2018-03-03,2018-04-04)', 'one'), +('[2,3)', '[2018-01-01,2018-01-05)', 'two'), +('[3,4)', '[2018-01-01,)', 'three'), +('[4,5)', '(,2018-04-01)', 'four'), +('[5,6)', '(,)', 'five') +; + +-- +-- UPDATE tests +-- + +-- Setting with a missing column fails +UPDATE for_portion_of_test +FOR PORTION OF invalid_at FROM '2018-06-01' TO MAXVALUE +SET name = 'foo' +WHERE id = '[5,6)'; + +-- Setting the range fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET valid_at = '[1990-01-01,1999-01-01)' +WHERE id = '[5,6)'; + +-- Setting with timestamps reversed fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' +SET name = 'three^1' +WHERE id = '[3,4)'; + +-- Setting with MAXVALUE lower bound fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MAXVALUE TO NULL +SET name = 'nope' +WHERE id = '[3,4)'; + +-- Setting with MINVALUE upper bound fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO MINVALUE +SET name = 'nope' +WHERE id = '[3,4)'; + +-- Setting with a subquery fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' +SET name = 'nope' +WHERE id = '[3,4)'; + +-- Setting with a column fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM lower(valid_at) TO MAXVALUE +SET name = 'nope' +WHERE id = '[3,4)'; + +-- Setting with timestamps equal does nothing +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' +SET name = 'three^0' +WHERE id = '[3,4)'; + +-- Updating a finite/open portion with a finite/open target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET name = 'three^1' +WHERE id = '[3,4)'; + +-- Updating a finite/open portion with an open/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO '2018-03-01' +SET name = 'three^2' +WHERE id = '[3,4)'; + +-- Updating an open/finite portion with an open/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO '2018-02-01' +SET name = 'four^1' +WHERE id = '[4,5)'; + +-- Updating an open/finite portion with a finite/open target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO MAXVALUE +SET name = 'four^2' +WHERE id = '[4,5)'; + +-- Updating a finite/finite portion with an exact fit +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01' +SET name = 'four^3' +WHERE id = '[4,5)'; + +-- Updating an enclosed span +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO MAXVALUE +SET name = 'two^2' +WHERE id = '[2,3)'; + +-- Updating an open/open portion with a finite/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' +SET name = 'five^2' +WHERE id = '[5,6)'; + +-- Updating an enclosed span with separate protruding spans +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01' +SET name = 'five^3' +WHERE id = '[5,6)'; + +-- Updating multiple enclosed spans +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO NULL +SET name = 'one^2' +WHERE id = '[1,2)'; + +-- Updating with a shift/reduce conflict +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE + TO '2019-01-01' +SET name = 'one^3' +WHERE id = '[1,2)'; + +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR + TO '2019-01-01' +SET name = 'one^4' +WHERE id = '[1,2)'; + +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR) + TO '2019-01-01' +SET name = 'one^4' +WHERE id = '[1,2)'; + +-- Updating the non-range part of the PK: +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-02-15' TO NULL +SET id = '[6,7)' +WHERE id = '[1,2)'; + +-- UPDATE with no WHERE clause +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2030-01-01' TO NULL +SET name = name || '*'; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- +-- DELETE tests +-- + +-- Deleting with a missing column fails +DELETE FROM for_portion_of_test +FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL +WHERE id = '[5,6)'; + +-- Deleting with timestamps reversed fails +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' +WHERE id = '[3,4)'; + +-- Deleting with timestamps equal does nothing +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' +WHERE id = '[3,4)'; + +-- Deleting with a closed/closed target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01' +WHERE id = '[5,6)'; + +-- Deleting with a closed/open target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO NULL +WHERE id = '[3,4)'; + +-- Deleting with an open/closed target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO '2018-02-08' +WHERE id = '[1,2)'; + +-- Deleting with an open/open target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO NULL +WHERE id = '[6,7)'; + +-- DELETE with no WHERE clause +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2025-01-01' TO NULL; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- UPDATE ... RETURNING returns only the updated values (not the inserted side values) +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15' +SET name = 'three^3' +WHERE id = '[3,4)' +RETURNING *; + +-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows + +CREATE FUNCTION for_portion_of_trigger() +RETURNS trigger +AS +$$ +BEGIN + RAISE NOTICE '% % % of %', TG_WHEN, TG_OP, NEW.valid_at, OLD.valid_at; + IF TG_OP = 'DELETE' THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; +END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER trg_for_portion_of_before_insert + BEFORE INSERT ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_after_insert + AFTER INSERT ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_before_update + BEFORE UPDATE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_after_update + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_before_delete + BEFORE DELETE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); +CREATE TRIGGER trg_for_portion_of_after_delete + AFTER DELETE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger(); + +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01' +SET name = 'five^4' +WHERE id = '[5,6)'; + +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01' +WHERE id = '[5,6)'; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_at; + +-- +-- Now re-run the same tests but with a PERIOD instead of a range: +-- + +DROP TABLE for_portion_of_test; + +-- Fails on tables without a temporal PK: +CREATE TABLE for_portion_of_test ( + id int4range PRIMARY KEY, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + name text NOT NULL +); + +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE +SET name = 'foo'; + +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-15' TO MAXVALUE; + +DROP TABLE for_portion_of_test; +CREATE TABLE for_portion_of_test ( + id int4range NOT NULL, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + name text NOT NULL, + CONSTRAINT for_portion_of_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO for_portion_of_test +VALUES +('[1,2)', '2018-01-02', '2018-02-03', 'one'), +('[1,2)', '2018-02-03', '2018-03-03', 'one'), +('[1,2)', '2018-03-03', '2018-04-04', 'one'), +('[2,3)', '2018-01-01', '2018-01-05', 'two'), +('[3,4)', '2018-01-01', null, 'three'), +('[4,5)', null, '2018-04-01', 'four'), +('[5,6)', null, null, 'five') +; + +-- +-- UPDATE tests +-- + +-- Setting with a missing column fails +UPDATE for_portion_of_test +FOR PORTION OF invalid_at FROM '2018-06-01' TO MAXVALUE +SET name = 'foo' +WHERE id = '[5,6)'; + +-- Setting the start column fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET valid_from = '1990-01-01' +WHERE id = '[5,6)'; + +-- Setting the end column fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET valid_til = '1999-01-01' +WHERE id = '[5,6)'; + +-- Setting with timestamps reversed fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' +SET name = 'three^1' +WHERE id = '[3,4)'; + +-- Setting with a subquery fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM (SELECT '2018-01-01') TO '2018-06-01' +SET name = 'nope' +WHERE id = '[3,4)'; + +-- Setting with a column fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM valid_from TO MAXVALUE +SET name = 'nope' +WHERE id = '[3,4)'; + +-- Setting with timestamps equal does nothing +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' +SET name = 'three^0' +WHERE id = '[3,4)'; + +-- Updating a finite/open portion with a finite/open target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE +SET name = 'three^1' +WHERE id = '[3,4)'; + +-- Updating a finite/open portion with an open/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO '2018-03-01' +SET name = 'three^2' +WHERE id = '[3,4)'; + +-- Updating an open/finite portion with an open/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO '2018-02-01' +SET name = 'four^1' +WHERE id = '[4,5)'; + +-- Updating an open/finite portion with a finite/open target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO MAXVALUE +SET name = 'four^2' +WHERE id = '[4,5)'; + +-- Updating a finite/finite portion with an exact fit +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO '2018-02-01' +SET name = 'four^3' +WHERE id = '[4,5)'; + +-- Updating an enclosed span +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM MINVALUE TO MAXVALUE +SET name = 'two^2' +WHERE id = '[2,3)'; + +-- Updating an open/open portion with a finite/finite target +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-01-01' TO '2019-01-01' +SET name = 'five^2' +WHERE id = '[5,6)'; + +-- Updating an enclosed span with separate protruding spans +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2017-01-01' TO '2020-01-01' +SET name = 'five^3' +WHERE id = '[5,6)'; + +-- Updating multiple enclosed spans +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO NULL +SET name = 'one^2' +WHERE id = '[1,2)'; + +-- Updating with a shift/reduce conflict +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM '2018-03-01' AT TIME ZONE INTERVAL '1' HOUR TO MINUTE + TO '2019-01-01' +SET name = 'one^3' +WHERE id = '[1,2)'; + +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM '2018-03-01' AT TIME ZONE INTERVAL '2' HOUR + TO '2019-01-01' +SET name = 'one^4' +WHERE id = '[1,2)'; + +UPDATE for_portion_of_test +FOR PORTION OF valid_at + FROM ('2018-03-01' AT TIME ZONE INTERVAL '2' HOUR) + TO '2019-01-01' +SET name = 'one^4' +WHERE id = '[1,2)'; + +-- Updating the non-range part of the PK: +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-02-15' TO NULL +SET id = '[6,7)' +WHERE id = '[1,2)'; + +-- UPDATE with no WHERE clause +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2030-01-01' TO NULL +SET name = name || '*'; + +-- TODO: UPDATE with generated columns too +SELECT * FROM for_portion_of_test ORDER BY id, valid_from NULLS FIRST, valid_til; + +-- +-- DELETE tests +-- + +-- Deleting with a missing column fails +DELETE FROM for_portion_of_test +FOR PORTION OF invalid_at FROM '2018-06-01' TO NULL +WHERE id = '[5,6)'; + +-- Deleting with timestamps reversed fails +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' +WHERE id = '[3,4)'; + +-- Deleting with timestamps equal does nothing +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-04-01' +WHERE id = '[3,4)'; + +-- Deleting with a closed/closed target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-06-01' TO '2020-06-01' +WHERE id = '[5,6)'; + +-- Deleting with a closed/open target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2018-04-01' TO NULL +WHERE id = '[3,4)'; + +-- Deleting with an open/closed target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO '2018-02-08' +WHERE id = '[1,2)'; + +-- Deleting with an open/open target +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM NULL TO NULL +WHERE id = '[6,7)'; + +-- DELETE with no WHERE clause +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2025-01-01' TO NULL; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_from NULLS FIRST, valid_til; + +-- UPDATE ... RETURNING returns only the updated values (not the inserted side values) +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2018-02-01' TO '2018-02-15' +SET name = 'three^3' +WHERE id = '[3,4)' +RETURNING *; + +-- test that we run triggers on the UPDATE/DELETEd row and the INSERTed rows + +CREATE FUNCTION for_portion_of_trigger2() +RETURNS trigger +AS +$$ +BEGIN + RAISE NOTICE '% % [%,%) of [%,%)', TG_WHEN, TG_OP, NEW.valid_from, NEW.valid_til, OLD.valid_from, OLD.valid_til; + IF TG_OP = 'DELETE' THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; +END; +$$ +LANGUAGE plpgsql; + +CREATE TRIGGER trg_for_portion_of_before_insert + BEFORE INSERT ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_after_insert + AFTER INSERT ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_before_update + BEFORE UPDATE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_after_update + AFTER UPDATE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_before_delete + BEFORE DELETE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); +CREATE TRIGGER trg_for_portion_of_after_delete + AFTER DELETE ON for_portion_of_test + FOR EACH ROW + EXECUTE FUNCTION for_portion_of_trigger2(); + +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM '2021-01-01' TO '2022-01-01' +SET name = 'five^4' +WHERE id = '[5,6)'; + +DELETE FROM for_portion_of_test +FOR PORTION OF valid_at FROM '2023-01-01' TO '2024-01-01' +WHERE id = '[5,6)'; + +SELECT * FROM for_portion_of_test ORDER BY id, valid_from NULLS FIRST, valid_til; -- 2.25.1 --------------AA1A9756402C5229DC7A97A9 Content-Type: text/x-patch; charset=UTF-8; name="v9-0004-Add-temporal-FOREIGN-KEYs.patch" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="v9-0004-Add-temporal-FOREIGN-KEYs.patch" From d8c9a5a72fc691f92d8afddb9cece66c380a17c9 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" Date: Sun, 27 Jun 2021 12:58:51 -0700 Subject: [PATCH v9 4/4] Add temporal FOREIGN KEYs - Added bison support for temporal FOREIGN KEYs. Like primary keys, they can have either a PERIOD or a range column for the temporal component. - Added temporal trigger functions akin to existing RI trigger functions. - Added support for ON {UPDATE,DELETE} {CASCADE,SET NULL,SET DEFAULT}. - Added pg_constraint.confperiod to record the oid of a PERIOD used in the other table. If an FK uses a range column instead, we just record its attnum with the other attnums used by the constraint. - We support foreign keys where one table uses a PERIOD and the other a range: you don't have to choose one or the other for your whole database. - Added pg_dump support. - Show the correct syntax in psql \d output for foreign keys. - Added tests and documentation. --- doc/src/sgml/ddl.sgml | 4 + doc/src/sgml/ref/create_table.sgml | 40 +- src/backend/catalog/heap.c | 1 + src/backend/catalog/index.c | 1 + src/backend/catalog/pg_constraint.c | 12 +- src/backend/commands/tablecmds.c | 891 +++++--- src/backend/commands/trigger.c | 2 + src/backend/commands/typecmds.c | 1 + src/backend/nodes/copyfuncs.c | 3 + src/backend/parser/gram.y | 31 +- src/backend/utils/adt/rangetypes.c | 29 + src/backend/utils/adt/ri_triggers.c | 1543 ++++++++++++-- src/backend/utils/adt/ruleutils.c | 17 +- src/include/catalog/pg_constraint.h | 10 +- src/include/catalog/pg_proc.dat | 44 + src/include/nodes/parsenodes.h | 2 + src/include/utils/rangetypes.h | 1 + src/test/regress/expected/for_portion_of.out | 9 + src/test/regress/expected/sanity_check.out | 1 + .../regress/expected/without_overlaps.out | 1846 +++++++++++++++++ src/test/regress/sql/for_portion_of.sql | 6 + src/test/regress/sql/without_overlaps.sql | 1402 +++++++++++++ 22 files changed, 5399 insertions(+), 497 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 57e0dc1363..baa2c101b7 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1192,6 +1192,10 @@ CREATE TABLE billing_addresses ( Any table with a temporal primary key supports temporal UPDATE and DELETE using FOR PORTION OF syntax. See and for details. + + + Temporal foreign keys have the same structure as temporal primary keys and must reference temporal primary keys. The referenced entity specified by the ordinary key parts must exist for at least as long as the value in the referencing row. Note that several rows in the referenced table may be required to completely satisfy a referencing value. + diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index fcf15013a7..7d64fc8348 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -88,7 +88,7 @@ PERIOD FOR { period_name | SYSTEM_T UNIQUE ( column_name [, ... ] [, temporal_interval WITHOUT OVERLAPS ] ) index_parameters | PRIMARY KEY ( column_name [, ... ] [, temporal_interval WITHOUT OVERLAPS ] ) index_parameters | EXCLUDE [ USING index_method ] ( exclude_element WITH operator [, ... ] ) index_parameters [ WHERE ( predicate ) ] | - FOREIGN KEY ( column_name [, ... ] ) REFERENCES reftable [ ( refcolumn [, ... ] ) ] + FOREIGN KEY ( column_name [, ... ] [, PERIOD temporal_interval ] ) REFERENCES reftable [ ( refcolumn [, ... ] [, PERIOD temporal_interval ] ) ] [ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ] [ ON DELETE referential_action ] [ ON UPDATE referential_action ] } [ DEFERRABLE | NOT DEFERRABLE ] [ INITIALLY DEFERRED | INITIALLY IMMEDIATE ] @@ -1141,8 +1141,8 @@ WITH ( MODULUS numeric_literal, REM REFERENCES reftable [ ( refcolumn ) ] [ MATCH matchtype ] [ ON DELETE referential_action ] [ ON UPDATE referential_action ] (column constraint) - FOREIGN KEY ( column_name [, ... ] ) - REFERENCES reftable [ ( refcolumn [, ... ] ) ] + FOREIGN KEY ( column_name [, ... ] [, PERIOD temporal_interval ] ) + REFERENCES reftable [ ( refcolumn [, ... ] [, PERIOD temporal_interval ] ) ] [ MATCH matchtype ] [ ON DELETE referential_action ] [ ON UPDATE referential_action ] @@ -1153,11 +1153,29 @@ WITH ( MODULUS numeric_literal, REM These clauses specify a foreign key constraint, which requires that a group of one or more columns of the new table must only contain values that match values in the referenced - column(s) of some row of the referenced table. If the refcolumn list is omitted, the primary key of the reftable is used. The referenced columns must be the columns of a non-deferrable - unique or primary key constraint in the referenced table. The user + unique or primary key constraint in the referenced table. + + + + If the last column is marked with PERIOD, + it must be a period or range column, and the referenced table + must have a temporal primary key. + The non-PERIOD columns are treated normally + (and there must be at least one of them), + but the PERIOD column is not compared for equality. + Instead the constraint is considered satisfied + if the referenced table has matching records whose combined ranges completely cover + the referencing record. + In other words, the reference must have a referent for its entire duration. + + + + The user must have REFERENCES permission on the referenced table (either the whole table, or the specific referenced columns). The addition of a foreign key constraint requires a @@ -1229,7 +1247,10 @@ WITH ( MODULUS numeric_literal, REM Delete any rows referencing the deleted row, or update the values of the referencing column(s) to the new values of the - referenced columns, respectively. + referenced columns, respectively. In a temporal foreign key, + the delete will use FOR PORTION OF semantics + to constrain the effect to the bounds of the referenced row + being deleted. @@ -1238,7 +1259,9 @@ WITH ( MODULUS numeric_literal, REM SET NULL - Set the referencing column(s) to null. + Set the referencing column(s) to null. In a temporal foreign key, + the change will use FOR PORTION OF semantics + to constrain the effect to the bounds of the referenced row. @@ -1250,6 +1273,9 @@ WITH ( MODULUS numeric_literal, REM Set the referencing column(s) to their default values. (There must be a row in the referenced table matching the default values, if they are not null, or the operation will fail.) + In a temporal foreign key, the change will use FOR PORTION + OF semantics to constrain the effect to the bounds of the + referenced row. diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index d8b99ade5c..e3b0089df9 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -2560,6 +2560,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr, is_no_inherit, /* connoinherit */ false, /* contemporal */ InvalidOid, /* conperiod */ + InvalidOid, /* confperiod */ is_internal); /* internally constructed? */ pfree(ccbin); diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 68c5700f6c..abffe387b7 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -2010,6 +2010,7 @@ index_constraint_create(Relation heapRelation, noinherit, is_temporal, /* contemporal */ periodid, /* conperiod */ + InvalidOid, /* confperiod */ is_internal); /* diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c index 7927fbde88..43dcdb3acc 100644 --- a/src/backend/catalog/pg_constraint.c +++ b/src/backend/catalog/pg_constraint.c @@ -78,6 +78,7 @@ CreateConstraintEntry(const char *constraintName, bool conNoInherit, bool conTemporal, Oid period, + Oid fperiod, bool is_internal) { Relation conDesc; @@ -190,6 +191,7 @@ CreateConstraintEntry(const char *constraintName, values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit); values[Anum_pg_constraint_contemporal - 1] = BoolGetDatum(conTemporal); values[Anum_pg_constraint_conperiod - 1] = ObjectIdGetDatum(period); + values[Anum_pg_constraint_confperiod - 1] = ObjectIdGetDatum(fperiod); if (conkeyArray) values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray); @@ -291,7 +293,7 @@ CreateConstraintEntry(const char *constraintName, { /* * Register normal dependency from constraint to foreign relation, or - * to specific column(s) if any are mentioned. + * to specific column(s) and period if any are mentioned. */ ObjectAddress relobject; @@ -309,6 +311,14 @@ CreateConstraintEntry(const char *constraintName, ObjectAddressSet(relobject, RelationRelationId, foreignRelId); add_exact_object_address(&relobject, addrs_normal); } + + if (OidIsValid(fperiod)) + { + ObjectAddress periodobject; + + ObjectAddressSet(periodobject, PeriodRelationId, fperiod); + add_exact_object_address(&periodobject, addrs_normal); + } } if (OidIsValid(indexRelId) && constraintType == CONSTRAINT_FOREIGN) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 88b692bd8d..4907cbf1ba 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -367,19 +367,24 @@ static ObjectAddress ATExecValidateConstraint(List **wqueue, bool recurse, bool recursing, LOCKMODE lockmode); static int transformColumnNameList(Oid relId, List *colList, int16 *attnums, Oid *atttypids); +static void transformPeriodName(Oid relId, Node *periodName, + int16 *attnums, Oid *atttypids, Oid *periodid); static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, List **attnamelist, int16 *attnums, Oid *atttypids, + Node **periodattname, Oid *periodid, + int16 *periodattnums, Oid *periodatttypids, Oid *opclasses); static Oid transformFkeyCheckAttrs(Relation pkrel, int numattrs, int16 *attnums, - Oid *opclasses); + bool is_temporal, int16 *periodattnums, + Oid periodid, Oid *opclasses); static void checkFkeyPermissions(Relation rel, int16 *attnums, int natts); static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId, Oid *funcid); static void validateForeignKeyConstraint(char *conname, Relation rel, Relation pkrel, - Oid pkindOid, Oid constraintOid); + Oid pkindOid, Oid constraintOid, bool temporal); static void ATController(AlterTableStmt *parsetree, Relation rel, List *cmds, bool recurse, LOCKMODE lockmode, AlterTableUtilityContext *context); @@ -493,12 +498,13 @@ static ObjectAddress addFkRecurseReferenced(List **wqueue, Constraint *fkconstra Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - bool old_check_ok); + bool old_check_ok, bool is_temporal, Oid pkperiod, Oid fkperiod); static void addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - bool old_check_ok, LOCKMODE lockmode); + bool old_check_ok, bool is_temporal, Oid pkperiod, Oid fkperiod, + LOCKMODE lockmode); static void CloneForeignKeyConstraints(List **wqueue, Relation parentRel, Relation partitionRel); static void CloneFkReferenced(Relation parentRel, Relation partitionRel); @@ -515,6 +521,12 @@ static bool tryAttachPartitionForeignKey(ForeignKeyCacheInfo *fk, Oid parentConstrOid, int numfks, AttrNumber *mapped_conkey, AttrNumber *confkey, Oid *conpfeqop); +static void FindFKComparisonOperators(Constraint *fkconstraint, + AlteredTableInfo *tab, int i, int16 *fkattnum, + bool *old_check_ok, ListCell **old_pfeqop_item, + Oid pktype, Oid fktype, Oid opclass, + bool is_temporal, bool for_overlaps, + Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut); static void ATExecDropConstraint(Relation rel, const char *constrName, DropBehavior behavior, bool recurse, bool recursing, @@ -5831,7 +5843,8 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode, validateForeignKeyConstraint(fkconstraint->conname, rel, refrel, con->refindid, - con->conid); + con->conid, + con->contemporal); /* * No need to mark the constraint row as validated, we did @@ -9503,11 +9516,18 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, Oid pfeqoperators[INDEX_MAX_KEYS]; Oid ppeqoperators[INDEX_MAX_KEYS]; Oid ffeqoperators[INDEX_MAX_KEYS]; + bool is_temporal = (fkconstraint->fk_period != NULL); + int16 pkperiodattnums[2]; + int16 fkperiodattnums[2]; + Oid pkperiodtypoids[2]; + Oid fkperiodtypoids[2]; int i; int numfks, numpks; Oid indexOid; bool old_check_ok; + Oid fkperiod = InvalidOid; + Oid pkperiod = InvalidOid; ObjectAddress address; ListCell *old_pfeqop_item = list_head(fkconstraint->old_conpfeqop); @@ -9605,6 +9625,21 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numfks = transformColumnNameList(RelationGetRelid(rel), fkconstraint->fk_attrs, fkattnum, fktypoid); + if (is_temporal) + { + /* + * First see if we have a period name, otherwise look for a range column. + * If it's a period, get the attnums of both start & end columns, + * but instead of their types get period's rangetype. + */ + transformPeriodName(RelationGetRelid(rel), + fkconstraint->fk_period, fkperiodattnums, fkperiodtypoids, &fkperiod); + + if (fkperiod == InvalidOid) + transformColumnNameList(RelationGetRelid(rel), + list_make1(fkconstraint->fk_period), + fkperiodattnums, fkperiodtypoids); + } /* * If the attribute list for the referenced table was omitted, lookup the @@ -9617,6 +9652,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numpks = transformFkeyGetPrimaryKey(pkrel, &indexOid, &fkconstraint->pk_attrs, pkattnum, pktypoid, + &fkconstraint->pk_period, + &pkperiod, + pkperiodattnums, pkperiodtypoids, opclasses); } else @@ -9624,8 +9662,19 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, numpks = transformColumnNameList(RelationGetRelid(pkrel), fkconstraint->pk_attrs, pkattnum, pktypoid); + if (is_temporal) { + transformPeriodName(RelationGetRelid(pkrel), + fkconstraint->pk_period, pkperiodattnums, pkperiodtypoids, &pkperiod); + + if (pkperiod == InvalidOid) + transformColumnNameList(RelationGetRelid(pkrel), + list_make1(fkconstraint->pk_period), + pkperiodattnums, pkperiodtypoids); + } /* Look for an index matching the column list */ indexOid = transformFkeyCheckAttrs(pkrel, numpks, pkattnum, + is_temporal, pkperiodattnums, + pkperiod, opclasses); } @@ -9684,187 +9733,43 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, for (i = 0; i < numpks; i++) { - Oid pktype = pktypoid[i]; - Oid fktype = fktypoid[i]; - Oid fktyped; - HeapTuple cla_ht; - Form_pg_opclass cla_tup; - Oid amid; - Oid opfamily; - Oid opcintype; - Oid pfeqop; - Oid ppeqop; - Oid ffeqop; - int16 eqstrategy; - Oid pfeqop_right; - - /* We need several fields out of the pg_opclass entry */ - cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclasses[i])); - if (!HeapTupleIsValid(cla_ht)) - elog(ERROR, "cache lookup failed for opclass %u", opclasses[i]); - cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht); - amid = cla_tup->opcmethod; - opfamily = cla_tup->opcfamily; - opcintype = cla_tup->opcintype; - ReleaseSysCache(cla_ht); - - /* - * Check it's a btree; currently this can never fail since no other - * index AMs support unique indexes. If we ever did have other types - * of unique indexes, we'd need a way to determine which operator - * strategy number is equality. (Is it reasonable to insist that - * every such index AM use btree's number for equality?) - */ - if (amid != BTREE_AM_OID) - elog(ERROR, "only b-tree indexes are supported for foreign keys"); - eqstrategy = BTEqualStrategyNumber; - - /* - * There had better be a primary equality operator for the index. - * We'll use it for PK = PK comparisons. - */ - ppeqop = get_opfamily_member(opfamily, opcintype, opcintype, - eqstrategy); - - if (!OidIsValid(ppeqop)) - elog(ERROR, "missing operator %d(%u,%u) in opfamily %u", - eqstrategy, opcintype, opcintype, opfamily); - - /* - * Are there equality operators that take exactly the FK type? Assume - * we should look through any domain here. - */ - fktyped = getBaseType(fktype); - - pfeqop = get_opfamily_member(opfamily, opcintype, fktyped, - eqstrategy); - if (OidIsValid(pfeqop)) + FindFKComparisonOperators( + fkconstraint, tab, i, fkattnum, + &old_check_ok, &old_pfeqop_item, + pktypoid[i], fktypoid[i], opclasses[i], + is_temporal, false, + &pfeqoperators[i], &ppeqoperators[i], &ffeqoperators[i]); + } + if (is_temporal) { + if (pkperiod == InvalidOid) { - pfeqop_right = fktyped; - ffeqop = get_opfamily_member(opfamily, fktyped, fktyped, - eqstrategy); + pkattnum[numpks] = pkperiodattnums[0]; + pktypoid[numpks] = pkperiodtypoids[0]; } else { - /* keep compiler quiet */ - pfeqop_right = InvalidOid; - ffeqop = InvalidOid; + pkattnum[numpks] = InvalidOid; + pktypoid[numpks] = InvalidOid; } - - if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop))) - { - /* - * Otherwise, look for an implicit cast from the FK type to the - * opcintype, and if found, use the primary equality operator. - * This is a bit tricky because opcintype might be a polymorphic - * type such as ANYARRAY or ANYENUM; so what we have to test is - * whether the two actual column types can be concurrently cast to - * that type. (Otherwise, we'd fail to reject combinations such - * as int[] and point[].) - */ - Oid input_typeids[2]; - Oid target_typeids[2]; - - input_typeids[0] = pktype; - input_typeids[1] = fktype; - target_typeids[0] = opcintype; - target_typeids[1] = opcintype; - if (can_coerce_type(2, input_typeids, target_typeids, - COERCION_IMPLICIT)) - { - pfeqop = ffeqop = ppeqop; - pfeqop_right = opcintype; - } - } - - if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop))) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("foreign key constraint \"%s\" cannot be implemented", - fkconstraint->conname), - errdetail("Key columns \"%s\" and \"%s\" " - "are of incompatible types: %s and %s.", - strVal(list_nth(fkconstraint->fk_attrs, i)), - strVal(list_nth(fkconstraint->pk_attrs, i)), - format_type_be(fktype), - format_type_be(pktype)))); - - if (old_check_ok) + if (fkperiod == InvalidOid) { - /* - * When a pfeqop changes, revalidate the constraint. We could - * permit intra-opfamily changes, but that adds subtle complexity - * without any concrete benefit for core types. We need not - * assess ppeqop or ffeqop, which RI_Initial_Check() does not use. - */ - old_check_ok = (pfeqop == lfirst_oid(old_pfeqop_item)); - old_pfeqop_item = lnext(fkconstraint->old_conpfeqop, - old_pfeqop_item); + fkattnum[numpks] = fkperiodattnums[0]; + fktypoid[numpks] = fkperiodtypoids[0]; } - if (old_check_ok) + else { - Oid old_fktype; - Oid new_fktype; - CoercionPathType old_pathtype; - CoercionPathType new_pathtype; - Oid old_castfunc; - Oid new_castfunc; - Form_pg_attribute attr = TupleDescAttr(tab->oldDesc, - fkattnum[i] - 1); - - /* - * Identify coercion pathways from each of the old and new FK-side - * column types to the right (foreign) operand type of the pfeqop. - * We may assume that pg_constraint.conkey is not changing. - */ - old_fktype = attr->atttypid; - new_fktype = fktype; - old_pathtype = findFkeyCast(pfeqop_right, old_fktype, - &old_castfunc); - new_pathtype = findFkeyCast(pfeqop_right, new_fktype, - &new_castfunc); - - /* - * Upon a change to the cast from the FK column to its pfeqop - * operand, revalidate the constraint. For this evaluation, a - * binary coercion cast is equivalent to no cast at all. While - * type implementors should design implicit casts with an eye - * toward consistency of operations like equality, we cannot - * assume here that they have done so. - * - * A function with a polymorphic argument could change behavior - * arbitrarily in response to get_fn_expr_argtype(). Therefore, - * when the cast destination is polymorphic, we only avoid - * revalidation if the input type has not changed at all. Given - * just the core data types and operator classes, this requirement - * prevents no would-be optimizations. - * - * If the cast converts from a base type to a domain thereon, then - * that domain type must be the opcintype of the unique index. - * Necessarily, the primary key column must then be of the domain - * type. Since the constraint was previously valid, all values on - * the foreign side necessarily exist on the primary side and in - * turn conform to the domain. Consequently, we need not treat - * domains specially here. - * - * Since we require that all collations share the same notion of - * equality (which they do, because texteq reduces to bitwise - * equality), we don't compare collation here. - * - * We need not directly consider the PK type. It's necessarily - * binary coercible to the opcintype of the unique index column, - * and ri_triggers.c will only deal with PK datums in terms of - * that opcintype. Changing the opcintype also changes pfeqop. - */ - old_check_ok = (new_pathtype == old_pathtype && - new_castfunc == old_castfunc && - (!IsPolymorphicType(pfeqop_right) || - new_fktype == old_fktype)); + fkattnum[numpks] = InvalidOid; + fktypoid[numpks] = InvalidOid; } - pfeqoperators[i] = pfeqop; - ppeqoperators[i] = ppeqop; - ffeqoperators[i] = ffeqop; + FindFKComparisonOperators( + fkconstraint, tab, numpks, fkattnum, + &old_check_ok, &old_pfeqop_item, + pkperiodtypoids[0], fkperiodtypoids[0], opclasses[numpks], + is_temporal, true, + &pfeqoperators[numpks], &ppeqoperators[numpks], &ffeqoperators[numpks]); + numfks += 1; + numpks += 1; } /* @@ -9880,7 +9785,10 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, pfeqoperators, ppeqoperators, ffeqoperators, - old_check_ok); + old_check_ok, + is_temporal, + pkperiod, + fkperiod); /* Now handle the referencing side. */ addFkRecurseReferencing(wqueue, fkconstraint, rel, pkrel, @@ -9893,6 +9801,9 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, ppeqoperators, ffeqoperators, old_check_ok, + is_temporal, + pkperiod, + fkperiod, lockmode); /* @@ -9933,7 +9844,8 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, - Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok) + Oid *ppeqoperators, Oid *ffeqoperators, bool old_check_ok, + bool is_temporal, Oid pkperiod, Oid fkperiod) { ObjectAddress address; Oid constrOid; @@ -10015,8 +9927,9 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, conislocal, /* islocal */ coninhcount, /* inhcount */ connoinherit, /* conNoInherit */ - false, /* conTemporal */ - InvalidOid, + is_temporal, + fkperiod, + pkperiod, false); /* is_internal */ ObjectAddressSet(address, ConstraintRelationId, constrOid); @@ -10091,7 +10004,7 @@ addFkRecurseReferenced(List **wqueue, Constraint *fkconstraint, Relation rel, partIndexId, constrOid, numfks, mapped_pkattnum, fkattnum, pfeqoperators, ppeqoperators, ffeqoperators, - old_check_ok); + old_check_ok, is_temporal, pkperiod, fkperiod); /* Done -- clean up (but keep the lock) */ table_close(partRel, NoLock); @@ -10140,7 +10053,8 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, Relation pkrel, Oid indexOid, Oid parentConstr, int numfks, int16 *pkattnum, int16 *fkattnum, Oid *pfeqoperators, Oid *ppeqoperators, Oid *ffeqoperators, - bool old_check_ok, LOCKMODE lockmode) + bool old_check_ok, bool is_temporal, Oid pkperiod, + Oid fkperiod, LOCKMODE lockmode) { AssertArg(OidIsValid(parentConstr)); @@ -10183,6 +10097,7 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, newcon->refrelid = RelationGetRelid(pkrel); newcon->refindid = indexOid; newcon->conid = parentConstr; + newcon->contemporal = fkconstraint->fk_period != NULL; newcon->qual = (Node *) fkconstraint; tab->constraints = lappend(tab->constraints, newcon); @@ -10285,8 +10200,9 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, false, 1, false, - false, /* conTemporal */ - InvalidOid, + is_temporal, + fkperiod, + pkperiod, false); /* @@ -10313,6 +10229,9 @@ addFkRecurseReferencing(List **wqueue, Constraint *fkconstraint, Relation rel, ppeqoperators, ffeqoperators, old_check_ok, + is_temporal, + pkperiod, + fkperiod, lockmode); table_close(partition, NoLock); @@ -10450,6 +10369,7 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) conpfeqop, conppeqop, conffeqop); + Assert(numfks == attmap->maplen); for (int i = 0; i < numfks; i++) mapped_confkey[i] = attmap->attnums[confkey[i] - 1]; @@ -10496,7 +10416,10 @@ CloneFkReferenced(Relation parentRel, Relation partitionRel) conpfeqop, conppeqop, conffeqop, - true); + true, + constrForm->contemporal, + constrForm->confperiod, + constrForm->conperiod); table_close(fkRel, NoLock); ReleaseSysCache(tuple); @@ -10689,8 +10612,9 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) false, /* islocal */ 1, /* inhcount */ false, /* conNoInherit */ - false, /* conTemporal */ - InvalidOid, + constrForm->contemporal, + constrForm->conperiod, + constrForm->confperiod, true); /* Set up partition dependencies for the new constraint */ @@ -10720,11 +10644,232 @@ CloneFkReferencing(List **wqueue, Relation parentRel, Relation partRel) conppeqop, conffeqop, false, /* no old check exists */ + constrForm->contemporal, + constrForm->conperiod, + constrForm->confperiod, AccessExclusiveLock); table_close(pkrel, NoLock); } } +static void +FindFKComparisonOperators(Constraint *fkconstraint, + AlteredTableInfo *tab, + int i, + int16 *fkattnum, + bool *old_check_ok, + ListCell **old_pfeqop_item, + Oid pktype, Oid fktype, Oid opclass, + bool is_temporal, bool for_overlaps, + Oid *pfeqopOut, Oid *ppeqopOut, Oid *ffeqopOut) +{ + Oid fktyped; + HeapTuple cla_ht; + Form_pg_opclass cla_tup; + Oid amid; + Oid opfamily; + Oid opcintype; + Oid pfeqop; + Oid ppeqop; + Oid ffeqop; + int16 eqstrategy; + Oid pfeqop_right; + + /* We need several fields out of the pg_opclass entry */ + cla_ht = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass)); + if (!HeapTupleIsValid(cla_ht)) + elog(ERROR, "cache lookup failed for opclass %u", opclass); + cla_tup = (Form_pg_opclass) GETSTRUCT(cla_ht); + amid = cla_tup->opcmethod; + opfamily = cla_tup->opcfamily; + opcintype = cla_tup->opcintype; + ReleaseSysCache(cla_ht); + + if (is_temporal) + { + if (amid != GIST_AM_OID) + elog(ERROR, "only GiST indexes are supported for temporal foreign keys"); + eqstrategy = for_overlaps ? RTOverlapStrategyNumber : RTEqualStrategyNumber; + } + else + { + /* + * Check it's a btree; currently this can never fail since no other + * index AMs support unique indexes. If we ever did have other types + * of unique indexes, we'd need a way to determine which operator + * strategy number is equality. (Is it reasonable to insist that + * every such index AM use btree's number for equality?) + */ + if (amid != BTREE_AM_OID) + elog(ERROR, "only b-tree indexes are supported for foreign keys"); + eqstrategy = BTEqualStrategyNumber; + } + + /* + * There had better be a primary equality operator for the index. + * We'll use it for PK = PK comparisons. + */ + ppeqop = get_opfamily_member(opfamily, opcintype, opcintype, + eqstrategy); + + if (!OidIsValid(ppeqop)) + elog(ERROR, "missing operator %d(%u,%u) in opfamily %u", + eqstrategy, opcintype, opcintype, opfamily); + + /* + * Are there equality operators that take exactly the FK type? Assume + * we should look through any domain here. + */ + fktyped = getBaseType(fktype); + + pfeqop = get_opfamily_member(opfamily, opcintype, fktyped, + eqstrategy); + if (OidIsValid(pfeqop)) + { + pfeqop_right = fktyped; + ffeqop = get_opfamily_member(opfamily, fktyped, fktyped, + eqstrategy); + } + else + { + /* keep compiler quiet */ + pfeqop_right = InvalidOid; + ffeqop = InvalidOid; + } + + if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop))) + { + /* + * Otherwise, look for an implicit cast from the FK type to the + * opcintype, and if found, use the primary equality operator. + * This is a bit tricky because opcintype might be a polymorphic + * type such as ANYARRAY or ANYENUM; so what we have to test is + * whether the two actual column types can be concurrently cast to + * that type. (Otherwise, we'd fail to reject combinations such + * as int[] and point[].) + */ + Oid input_typeids[2]; + Oid target_typeids[2]; + + input_typeids[0] = pktype; + input_typeids[1] = fktype; + target_typeids[0] = opcintype; + target_typeids[1] = opcintype; + if (can_coerce_type(2, input_typeids, target_typeids, + COERCION_IMPLICIT)) + { + pfeqop = ffeqop = ppeqop; + pfeqop_right = opcintype; + } + } + + if (!(OidIsValid(pfeqop) && OidIsValid(ffeqop))) + { + char *fkattr_name; + char *pkattr_name; + + if (for_overlaps) + { + fkattr_name = strVal(fkconstraint->fk_period); + pkattr_name = strVal(fkconstraint->pk_period); + } + else + { + fkattr_name = strVal(list_nth(fkconstraint->fk_attrs, i)); + pkattr_name = strVal(list_nth(fkconstraint->pk_attrs, i)); + } + + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("foreign key constraint \"%s\" cannot be implemented", + fkconstraint->conname), + errdetail("Key columns \"%s\" and \"%s\" " + "are of incompatible types: %s and %s.", + fkattr_name, + pkattr_name, + format_type_be(fktype), + format_type_be(pktype)))); + } + + if (*old_check_ok) + { + /* + * When a pfeqop changes, revalidate the constraint. We could + * permit intra-opfamily changes, but that adds subtle complexity + * without any concrete benefit for core types. We need not + * assess ppeqop or ffeqop, which RI_Initial_Check() does not use. + */ + *old_check_ok = (pfeqop == lfirst_oid(*old_pfeqop_item)); + *old_pfeqop_item = lnext(fkconstraint->old_conpfeqop, + *old_pfeqop_item); + } + if (*old_check_ok) + { + Oid old_fktype; + Oid new_fktype; + CoercionPathType old_pathtype; + CoercionPathType new_pathtype; + Oid old_castfunc; + Oid new_castfunc; + Form_pg_attribute attr = TupleDescAttr(tab->oldDesc, + fkattnum[i] - 1); + + /* + * Identify coercion pathways from each of the old and new FK-side + * column types to the right (foreign) operand type of the pfeqop. + * We may assume that pg_constraint.conkey is not changing. + */ + old_fktype = attr->atttypid; + new_fktype = fktype; + old_pathtype = findFkeyCast(pfeqop_right, old_fktype, + &old_castfunc); + new_pathtype = findFkeyCast(pfeqop_right, new_fktype, + &new_castfunc); + + /* + * Upon a change to the cast from the FK column to its pfeqop + * operand, revalidate the constraint. For this evaluation, a + * binary coercion cast is equivalent to no cast at all. While + * type implementors should design implicit casts with an eye + * toward consistency of operations like equality, we cannot + * assume here that they have done so. + * + * A function with a polymorphic argument could change behavior + * arbitrarily in response to get_fn_expr_argtype(). Therefore, + * when the cast destination is polymorphic, we only avoid + * revalidation if the input type has not changed at all. Given + * just the core data types and operator classes, this requirement + * prevents no would-be optimizations. + * + * If the cast converts from a base type to a domain thereon, then + * that domain type must be the opcintype of the unique index. + * Necessarily, the primary key column must then be of the domain + * type. Since the constraint was previously valid, all values on + * the foreign side necessarily exist on the primary side and in + * turn conform to the domain. Consequently, we need not treat + * domains specially here. + * + * Since we require that all collations share the same notion of + * equality (which they do, because texteq reduces to bitwise + * equality), we don't compare collation here. + * + * We need not directly consider the PK type. It's necessarily + * binary coercible to the opcintype of the unique index column, + * and ri_triggers.c will only deal with PK datums in terms of + * that opcintype. Changing the opcintype also changes pfeqop. + */ + *old_check_ok = (new_pathtype == old_pathtype && + new_castfunc == old_castfunc && + (!IsPolymorphicType(pfeqop_right) || + new_fktype == old_fktype)); + + } + + *pfeqopOut = pfeqop; + *ppeqopOut = ppeqop; + *ffeqopOut = ffeqop; +} + /* * When the parent of a partition receives [the referencing side of] a foreign * key, we must propagate that foreign key to the partition. However, the @@ -11337,6 +11482,38 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName, return address; } +/* + * transformPeriodName - transform period name to its base columns + * + * Lookup the name and return zero as the column attnum. Set + * the period's rangetype (not the columns' type OIDs). + */ +static void +transformPeriodName(Oid relId, Node *periodName, + int16 *attnums, Oid *atttypids, Oid *periodid) +{ + const char *periodStr = strVal(periodName); + HeapTuple pertuple = SearchSysCache2(PERIODNAME, + ObjectIdGetDatum(relId), + PointerGetDatum(periodStr)); + Form_pg_period period; + + if (!HeapTupleIsValid(pertuple)) + { + attnums[0] = InvalidOid; + atttypids[0] = InvalidOid; + *periodid = InvalidOid; + return; + } + + period = (Form_pg_period) GETSTRUCT(pertuple); + + attnums[0] = InvalidOid; + atttypids[0] = period->perrngtype; + *periodid = period->oid; + + ReleaseSysCache(pertuple); +} /* * transformColumnNameList - transform list of column names @@ -11362,6 +11539,7 @@ transformColumnNameList(Oid relId, List *colList, (errcode(ERRCODE_UNDEFINED_COLUMN), errmsg("column \"%s\" referenced in foreign key constraint does not exist", attname))); + if (attnum >= INDEX_MAX_KEYS) ereport(ERROR, (errcode(ERRCODE_TOO_MANY_COLUMNS), @@ -11383,6 +11561,9 @@ transformColumnNameList(Oid relId, List *colList, * for the pkrel. Also return the index OID and index opclasses of the * index supporting the primary key. If this is a temporal primary key, * also set the WITHOUT OVERLAPS attribute name, attnum, and atttypid. + * For ranges we only set one thing for each of those. For periods we set + * the start/end column names and attnums, but the typids we set to the + * period's range type. * * All parameters except pkrel are output parameters. Also, the function * return value is the number of attributes in the primary key, @@ -11394,6 +11575,8 @@ static int transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, List **attnamelist, int16 *attnums, Oid *atttypids, + Node **periodattname, Oid *periodid, + int16 *periodattnums, Oid *periodatttypids, Oid *opclasses) { List *indexoidlist; @@ -11460,36 +11643,84 @@ transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid, /* * Now build the list of PK attributes from the indkey definition (we - * assume a primary key cannot have expressional elements) + * assume a primary key cannot have expressional elements, unless it + * has a PERIOD) */ *attnamelist = NIL; for (i = 0; i < indexStruct->indnkeyatts; i++) { int pkattno = indexStruct->indkey.values[i]; - attnums[i] = pkattno; - atttypids[i] = attnumTypeId(pkrel, pkattno); - opclasses[i] = indclass->values[i]; - *attnamelist = lappend(*attnamelist, - makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno))))); + if (i == indexStruct->indnkeyatts - 1 && indexStruct->indisexclusion) + { + if (pkattno == InvalidOid) + { + /* we have a period */ + HeapTuple periodTuple = SearchSysCache1(PERIODOID, ObjectIdGetDatum(indexStruct->indperiod)); + Form_pg_period period; + + if (!HeapTupleIsValid(periodTuple)) + elog(ERROR, "cache lookup failed for period %u", indexStruct->indperiod); + + period = (Form_pg_period) GETSTRUCT(periodTuple); + attnums[i] = InvalidOid; + periodattnums[0] = period->perstart; + periodattnums[1] = period->perend; + periodatttypids[0] = period->perrngtype; + periodatttypids[1] = period->perrngtype; + opclasses[i] = indclass->values[i]; + *periodid = period->oid; + *periodattname = (Node *)makeString(pstrdup(NameStr(period->pername))); + + ReleaseSysCache(periodTuple); + } + else { + /* we have a range */ + /* The caller will set attnums[i] */ + periodattnums[0] = pkattno; + periodatttypids[0] = attnumTypeId(pkrel, pkattno); + opclasses[i] = indclass->values[i]; + *periodid = InvalidOid; + *periodattname = (Node *)makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno)))); + } + } + else + { + attnums[i] = pkattno; + atttypids[i] = attnumTypeId(pkrel, pkattno); + opclasses[i] = indclass->values[i]; + *attnamelist = lappend(*attnamelist, + makeString(pstrdup(NameStr(*attnumAttName(pkrel, pkattno))))); + *periodid = InvalidOid; + } } ReleaseSysCache(indexTuple); - return i; + if (indexStruct->indisexclusion) return i - 1; + else return i; } /* * transformFkeyCheckAttrs - * * Make sure that the attributes of a referenced table belong to a unique - * (or primary key) constraint. Return the OID of the index supporting - * the constraint, as well as the opclasses associated with the index + * (or primary key) constraint. Or if this is a temporal foreign key + * the primary key should be an exclusion constraint instead. + * It's okay to have a PERIOD referencing a range or a range referencing + * a PERIOD, as long as the types match up. If the PK uses a PERIOD, + * then periodid should be set and periodattnums should contain the + * start and end attnums. Otherwise periodattnums should contain the + * range attnum. + * Return the OID of the index supporting the constraint, + * as well as the opclasses associated with the index * columns. */ static Oid transformFkeyCheckAttrs(Relation pkrel, int numattrs, int16 *attnums, + bool is_temporal, int16 *periodattnums, + Oid periodid, Oid *opclasses) /* output parameter */ { Oid indexoid = InvalidOid; @@ -11499,6 +11730,7 @@ transformFkeyCheckAttrs(Relation pkrel, ListCell *indexoidscan; int i, j; + bool has_period = periodid != InvalidOid; /* * Reject duplicate appearances of columns in the referenced-columns list. @@ -11516,6 +11748,10 @@ transformFkeyCheckAttrs(Relation pkrel, (errcode(ERRCODE_INVALID_FOREIGN_KEY), errmsg("foreign key referenced-columns list must not contain duplicates"))); } + if (is_temporal && attnums[i] == periodattnums[0]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_FOREIGN_KEY), + errmsg("foreign key referenced-columns list must not contain duplicates"))); } /* @@ -11537,15 +11773,19 @@ transformFkeyCheckAttrs(Relation pkrel, indexStruct = (Form_pg_index) GETSTRUCT(indexTuple); /* - * Must have the right number of columns; must be unique and not a - * partial index; forget it if there are any expressions, too. Invalid - * indexes are out as well. + * Must have the right number of columns; must be unique + * (or if temporal then exclusion instead) and not a + * partial index; forget it if there are any expressions, too (unless + * it has a PERIOD). Invalid indexes are out as well. */ - if (indexStruct->indnkeyatts == numattrs && - indexStruct->indisunique && + if ((is_temporal + ? (indexStruct->indnkeyatts == numattrs + 1 && + indexStruct->indisexclusion) + : (indexStruct->indnkeyatts == numattrs && + indexStruct->indisunique)) && indexStruct->indisvalid && heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL) && - heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL)) + (indexStruct->indperiod != InvalidOid || heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))) { Datum indclassDatum; bool isnull; @@ -11582,6 +11822,32 @@ transformFkeyCheckAttrs(Relation pkrel, if (!found) break; } + if (found && is_temporal) + { + if (has_period) + { + if (periodid == indexStruct->indperiod) + { + found = true; + opclasses[numattrs] = indclass->values[numattrs - 1]; + } + else + found = false; + } + else + { + found = false; + for (j = 0; j < numattrs + 1; j++) + { + if (periodattnums[0] == indexStruct->indkey.values[j]) + { + opclasses[numattrs] = indclass->values[j]; + found = true; + break; + } + } + } + } /* * Refuse to use a deferrable unique/primary key. This is per SQL @@ -11691,7 +11957,8 @@ validateForeignKeyConstraint(char *conname, Relation rel, Relation pkrel, Oid pkindOid, - Oid constraintOid) + Oid constraintOid, + bool temporal) { TupleTableSlot *slot; TableScanDesc scan; @@ -11721,8 +11988,10 @@ validateForeignKeyConstraint(char *conname, /* * See if we can do it with a single LEFT JOIN query. A false result * indicates we must proceed with the fire-the-trigger method. + * We can't do a LEFT JOIN for temporal FKs yet, + * but we can once we support temporal left joins. */ - if (RI_Initial_Check(&trig, rel, pkrel)) + if (!temporal && RI_Initial_Check(&trig, rel, pkrel)) return; /* @@ -11783,6 +12052,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, Oid constraintOid, Oid indexOid, bool on_insert) { CreateTrigStmt *fk_trigger; + bool is_temporal = fkconstraint->fk_period; /* * Note: for a self-referential FK (referencing and referenced tables are @@ -11796,18 +12066,27 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint, fk_trigger = makeNode(CreateTrigStmt); fk_trigger->replace = false; fk_trigger->isconstraint = true; - fk_trigger->trigname = "RI_ConstraintTrigger_c"; + if (is_temporal) + fk_trigger->trigname = "TRI_ConstraintTrigger_c"; + else + fk_trigger->trigname = "RI_ConstraintTrigger_c"; fk_trigger->relation = NULL; /* Either ON INSERT or ON UPDATE */ if (on_insert) { - fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins"); + if (is_temporal) + fk_trigger->funcname = SystemFuncName("TRI_FKey_check_ins"); + else + fk_trigger->funcname = SystemFuncName("RI_FKey_check_ins"); fk_trigger->events = TRIGGER_TYPE_INSERT; } else { - fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd"); + if (is_temporal) + fk_trigger->funcname = SystemFuncName("TRI_FKey_check_upd"); + else + fk_trigger->funcname = SystemFuncName("RI_FKey_check_upd"); fk_trigger->events = TRIGGER_TYPE_UPDATE; } @@ -11856,37 +12135,76 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr fk_trigger->whenClause = NULL; fk_trigger->transitionRels = NIL; fk_trigger->constrrel = NULL; - switch (fkconstraint->fk_del_action) + if (fkconstraint->fk_period != NULL) { - case FKCONSTR_ACTION_NOACTION: - fk_trigger->deferrable = fkconstraint->deferrable; - fk_trigger->initdeferred = fkconstraint->initdeferred; - fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del"); - break; - case FKCONSTR_ACTION_RESTRICT: - fk_trigger->deferrable = false; - fk_trigger->initdeferred = false; - fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del"); - break; - case FKCONSTR_ACTION_CASCADE: - fk_trigger->deferrable = false; - fk_trigger->initdeferred = false; - fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del"); - break; - case FKCONSTR_ACTION_SETNULL: - fk_trigger->deferrable = false; - fk_trigger->initdeferred = false; - fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del"); - break; - case FKCONSTR_ACTION_SETDEFAULT: - fk_trigger->deferrable = false; - fk_trigger->initdeferred = false; - fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del"); - break; - default: - elog(ERROR, "unrecognized FK action type: %d", - (int) fkconstraint->fk_del_action); - break; + /* Temporal foreign keys */ + switch (fkconstraint->fk_del_action) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fkconstraint->deferrable; + fk_trigger->initdeferred = fkconstraint->initdeferred; + fk_trigger->funcname = SystemFuncName("TRI_FKey_noaction_del"); + break; + case FKCONSTR_ACTION_RESTRICT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_del"); + break; + case FKCONSTR_ACTION_CASCADE: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("TRI_FKey_cascade_del"); + break; + case FKCONSTR_ACTION_SETNULL: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("TRI_FKey_setnull_del"); + break; + case FKCONSTR_ACTION_SETDEFAULT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("TRI_FKey_setdefault_del"); + break; + default: + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_del_action); + break; + } + } + else + { + switch (fkconstraint->fk_del_action) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fkconstraint->deferrable; + fk_trigger->initdeferred = fkconstraint->initdeferred; + fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del"); + break; + case FKCONSTR_ACTION_RESTRICT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del"); + break; + case FKCONSTR_ACTION_CASCADE: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del"); + break; + case FKCONSTR_ACTION_SETNULL: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del"); + break; + case FKCONSTR_ACTION_SETDEFAULT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del"); + break; + default: + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_del_action); + break; + } } (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel), @@ -11913,37 +12231,76 @@ createForeignKeyActionTriggers(Relation rel, Oid refRelOid, Constraint *fkconstr fk_trigger->whenClause = NULL; fk_trigger->transitionRels = NIL; fk_trigger->constrrel = NULL; - switch (fkconstraint->fk_upd_action) + if (fkconstraint->fk_period != NULL) { - case FKCONSTR_ACTION_NOACTION: - fk_trigger->deferrable = fkconstraint->deferrable; - fk_trigger->initdeferred = fkconstraint->initdeferred; - fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd"); - break; - case FKCONSTR_ACTION_RESTRICT: - fk_trigger->deferrable = false; - fk_trigger->initdeferred = false; - fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd"); - break; - case FKCONSTR_ACTION_CASCADE: - fk_trigger->deferrable = false; - fk_trigger->initdeferred = false; - fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd"); - break; - case FKCONSTR_ACTION_SETNULL: - fk_trigger->deferrable = false; - fk_trigger->initdeferred = false; - fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd"); - break; - case FKCONSTR_ACTION_SETDEFAULT: - fk_trigger->deferrable = false; - fk_trigger->initdeferred = false; - fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd"); - break; - default: - elog(ERROR, "unrecognized FK action type: %d", - (int) fkconstraint->fk_upd_action); - break; + /* Temporal foreign keys */ + switch (fkconstraint->fk_upd_action) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fkconstraint->deferrable; + fk_trigger->initdeferred = fkconstraint->initdeferred; + fk_trigger->funcname = SystemFuncName("TRI_FKey_noaction_upd"); + break; + case FKCONSTR_ACTION_RESTRICT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("TRI_FKey_restrict_upd"); + break; + case FKCONSTR_ACTION_CASCADE: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("TRI_FKey_cascade_upd"); + break; + case FKCONSTR_ACTION_SETNULL: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("TRI_FKey_setnull_upd"); + break; + case FKCONSTR_ACTION_SETDEFAULT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("TRI_FKey_setdefault_upd"); + break; + default: + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_upd_action); + break; + } + } + else + { + switch (fkconstraint->fk_upd_action) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fkconstraint->deferrable; + fk_trigger->initdeferred = fkconstraint->initdeferred; + fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd"); + break; + case FKCONSTR_ACTION_RESTRICT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd"); + break; + case FKCONSTR_ACTION_CASCADE: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd"); + break; + case FKCONSTR_ACTION_SETNULL: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd"); + break; + case FKCONSTR_ACTION_SETDEFAULT: + fk_trigger->deferrable = false; + fk_trigger->initdeferred = false; + fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd"); + break; + default: + elog(ERROR, "unrecognized FK action type: %d", + (int) fkconstraint->fk_upd_action); + break; + } } (void) CreateTrigger(fk_trigger, NULL, refRelOid, RelationGetRelid(rel), diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index a503d638be..cd5653e317 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -839,6 +839,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString, true, /* noinherit */ false, /* contemporal */ InvalidOid, /* conperiod */ + InvalidOid, /* confperiod */ isInternal); /* is_internal */ } @@ -3885,6 +3886,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events, newshared->ats_relid == evtshared->ats_relid && newshared->ats_event == evtshared->ats_event && newshared->ats_table == evtshared->ats_table && + newshared->for_portion_of == evtshared->for_portion_of && newshared->ats_firing_id == 0) break; } diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index ec48ac32c7..1bb400a482 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -3554,6 +3554,7 @@ domainAddConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid, false, /* connoinherit */ false, /* contemporal */ InvalidOid, /* conperiod */ + InvalidOid, /* confperiod */ false); /* is_internal */ if (constrAddr) ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid); diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 833212fb7f..d34e590f84 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3118,7 +3118,9 @@ _copyConstraint(const Constraint *from) COPY_NODE_FIELD(where_clause); COPY_NODE_FIELD(pktable); COPY_NODE_FIELD(fk_attrs); + COPY_NODE_FIELD(fk_period); COPY_NODE_FIELD(pk_attrs); + COPY_NODE_FIELD(pk_period); COPY_SCALAR_FIELD(fk_matchtype); COPY_SCALAR_FIELD(fk_upd_action); COPY_SCALAR_FIELD(fk_del_action); @@ -3220,6 +3222,7 @@ _copyQuery(const Query *from) COPY_SCALAR_FIELD(canSetTag); COPY_NODE_FIELD(utilityStmt); COPY_SCALAR_FIELD(resultRelation); + COPY_NODE_FIELD(forPortionOf); COPY_SCALAR_FIELD(hasAggs); COPY_SCALAR_FIELD(hasWindowFuncs); COPY_SCALAR_FIELD(hasTargetSRFs); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index d7ffb1d7bb..81a3cec904 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -499,11 +499,12 @@ 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 withoutOverlapsClause +%type def_arg columnElem withoutOverlapsClause optionalPeriodName where_clause where_or_current_clause a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound columnref in_expr having_clause func_table xmltable array_expr OptWhereClause operator_def_arg +%type opt_column_and_period_list %type rowsfrom_item rowsfrom_list opt_col_def_list %type opt_ordinality %type ExclusionConstraintList ExclusionConstraintElem @@ -3941,19 +3942,21 @@ ConstraintElem: NULL, yyscanner); $$ = (Node *)n; } - | FOREIGN KEY '(' columnList ')' REFERENCES qualified_name - opt_column_list key_match key_actions ConstraintAttributeSpec + | FOREIGN KEY '(' columnList optionalPeriodName ')' REFERENCES qualified_name + opt_column_and_period_list key_match key_actions ConstraintAttributeSpec { Constraint *n = makeNode(Constraint); n->contype = CONSTR_FOREIGN; n->location = @1; - n->pktable = $7; + n->pktable = $8; n->fk_attrs = $4; - n->pk_attrs = $8; - n->fk_matchtype = $9; - n->fk_upd_action = (char) ($10 >> 8); - n->fk_del_action = (char) ($10 & 0xFF); - processCASbits($11, @11, "FOREIGN KEY", + n->fk_period = $5; + n->pk_attrs = linitial($9); + n->pk_period = lsecond($9); + n->fk_matchtype = $10; + n->fk_upd_action = (char) ($11 >> 8); + n->fk_del_action = (char) ($11 & 0xFF); + processCASbits($12, @12, "FOREIGN KEY", &n->deferrable, &n->initdeferred, &n->skip_validation, NULL, yyscanner); @@ -3981,6 +3984,16 @@ withoutOverlapsClause: | /*EMPTY*/ { $$ = NULL; } ; +optionalPeriodName: + ',' PERIOD columnElem { $$ = $3; } + | /*EMPTY*/ { $$ = NULL; } + ; + +opt_column_and_period_list: + '(' columnList optionalPeriodName ')' { $$ = list_make2($2, $3); } + | /*EMPTY*/ { $$ = list_make2(NIL, NULL); } + ; + columnElem: ColId { $$ = (Node *) makeString($1); diff --git a/src/backend/utils/adt/rangetypes.c b/src/backend/utils/adt/rangetypes.c index 5a38178485..1594fe0e29 100644 --- a/src/backend/utils/adt/rangetypes.c +++ b/src/backend/utils/adt/rangetypes.c @@ -2130,6 +2130,35 @@ make_empty_range(TypeCacheEntry *typcache) return make_range(typcache, &lower, &upper, true); } +/* + * Convert a range to a string. This isn't used anywhere but it handy for + * debugging. Perhaps we should remove it? + */ +char * +range_as_string(RangeType *r) +{ + int16 typlen; + bool typbyval; + char typalign; + char typdelim; + char *rangeStr; + Oid typioparam; + Oid range_out_oid; + + get_type_io_data( + RangeTypeGetOid(r), + IOFunc_output, + &typlen, + &typbyval, + &typalign, + &typdelim, + &typioparam, + &range_out_oid); + rangeStr = OidOutputFunctionCall(range_out_oid, RangeTypePGetDatum(r)); + + return rangeStr; +} + /* *---------------------------------------------------------- diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 96269fc2ad..4606ee81d0 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -31,6 +31,8 @@ #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" #include "catalog/pg_operator.h" +#include "catalog/pg_period.h" +#include "catalog/pg_range.h" #include "catalog/pg_type.h" #include "commands/trigger.h" #include "executor/executor.h" @@ -78,6 +80,10 @@ #define RI_PLAN_RESTRICT_CHECKREF 5 #define RI_PLAN_SETNULL_DOUPDATE 6 #define RI_PLAN_SETDEFAULT_DOUPDATE 7 +#define TRI_PLAN_CASCADE_DEL_DODELETE 8 +#define TRI_PLAN_CASCADE_UPD_DOUPDATE 9 +#define TRI_PLAN_SETNULL_DOUPDATE 10 +#define TRI_PLAN_SETDEFAULT_DOUPDATE 11 #define MAX_QUOTED_NAME_LEN (NAMEDATALEN*2+3) #define MAX_QUOTED_REL_NAME_LEN (MAX_QUOTED_NAME_LEN*2) @@ -111,7 +117,18 @@ typedef struct RI_ConstraintInfo char confupdtype; /* foreign key's ON UPDATE action */ char confdeltype; /* foreign key's ON DELETE action */ char confmatchtype; /* foreign key's match type */ + bool temporal; /* if the foreign key is temporal */ int nkeys; /* number of key columns */ + Oid pk_period; /* set if the temporal primary key has a period */ + Oid fk_period; /* set if the temporal foreign key has a period */ + Oid pk_period_rangetype; /* set if the temporal primary key has a period */ + Oid fk_period_rangetype; /* set if the temporal foreign key has a period */ + char pk_period_rangetype_name[NAMEDATALEN]; /* pk period's rangetype's name */ + char fk_period_rangetype_name[NAMEDATALEN]; /* fk period's rangetype's name */ + Oid pk_period_collation; /* pk period's rangetype's collation */ + Oid fk_period_collation; /* fk period's rangetype's collation */ + int16 pk_period_attnums[2]; /* attnums of the referenced period cols */ + int16 fk_period_attnums[2]; /* attnums of the referencing period cols */ int16 pk_attnums[RI_MAX_NUMKEYS]; /* attnums of referenced cols */ int16 fk_attnums[RI_MAX_NUMKEYS]; /* attnums of referencing cols */ Oid pf_eq_oprs[RI_MAX_NUMKEYS]; /* equality operators (PK = FK) */ @@ -181,6 +198,7 @@ static bool ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel, const RI_ConstraintInfo *riinfo); static Datum ri_restrict(TriggerData *trigdata, bool is_no_action); static Datum ri_set(TriggerData *trigdata, bool is_set_null); +static Datum tri_set(TriggerData *trigdata, bool is_set_null); static void quoteOneName(char *buffer, const char *name); static void quoteRelationName(char *buffer, Relation rel); static void ri_GenerateQual(StringInfo buf, @@ -194,7 +212,7 @@ static int ri_NullCheck(TupleDesc tupdesc, TupleTableSlot *slot, static void ri_BuildQueryKey(RI_QueryKey *key, const RI_ConstraintInfo *riinfo, int32 constr_queryno); -static bool ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, +static bool ri_KeysStable(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, const RI_ConstraintInfo *riinfo, bool rel_is_pk); static bool ri_AttributesEqual(Oid eq_opr, Oid typeid, Datum oldvalue, Datum newvalue); @@ -209,6 +227,7 @@ static void ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname, int tgkind); static const RI_ConstraintInfo *ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk); +static void DeconstructFkConstraintPeriod(Oid periodid, Oid *rangetypeid, char *rangetypename, Oid *rngcollation, int16 *attnums); static const RI_ConstraintInfo *ri_LoadConstraintInfo(Oid constraintOid); static Oid get_ri_constraint_root(Oid constrOid); static SPIPlanPtr ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes, @@ -217,16 +236,99 @@ static bool ri_PerformCheck(const RI_ConstraintInfo *riinfo, RI_QueryKey *qkey, SPIPlanPtr qplan, Relation fk_rel, Relation pk_rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, + bool hasForPortionOf, Datum forPortionOf, bool detectNewRows, int expect_OK); static void ri_ExtractValues(Relation rel, TupleTableSlot *slot, const RI_ConstraintInfo *riinfo, bool rel_is_pk, + bool hasForPortionOf, Datum forPortionOf, Datum *vals, char *nulls); static void ri_ReportViolation(const RI_ConstraintInfo *riinfo, Relation pk_rel, Relation fk_rel, TupleTableSlot *violatorslot, TupleDesc tupdesc, int queryno, bool partgone) pg_attribute_noreturn(); +static Datum tupleRange(TupleTableSlot *slot, const RI_ConstraintInfo *riinfo); +/* + * query_period_range + * + * Builds a SQL string to construct a range from a period's start and end columns. + */ +static char * +query_period_range(const RI_ConstraintInfo *riinfo, Relation rel, const char *table_name, bool rel_is_pk) +{ + StringInfo str = makeStringInfo(); + char attname[MAX_QUOTED_NAME_LEN + 3]; + int table_name_len = strlen(table_name); + + Assert(table_name_len <= 3); + + strcpy(attname, table_name); + if (rel_is_pk) + { + appendStringInfo(str, "%s(", riinfo->pk_period_rangetype_name); + + quoteOneName(attname + table_name_len, + RIAttName(rel, riinfo->pk_period_attnums[0])); + appendStringInfo(str, "%s, ", attname); + + quoteOneName(attname + table_name_len, + RIAttName(rel, riinfo->pk_period_attnums[1])); + appendStringInfo(str, "%s)", attname); + } + else + { + appendStringInfo(str, "%s(", riinfo->fk_period_rangetype_name); + + quoteOneName(attname + table_name_len, + RIAttName(rel, riinfo->fk_period_attnums[0])); + appendStringInfo(str, "%s, ", attname); + + quoteOneName(attname + table_name_len, + RIAttName(rel, riinfo->fk_period_attnums[1])); + appendStringInfo(str, "%s)", attname); + } + + return pstrdup(str->data); +} + + +static Datum +build_period_range(const RI_ConstraintInfo *riinfo, TupleTableSlot *slot, bool rel_is_pk) +{ + const int16 *period_attnums = rel_is_pk ? riinfo->pk_period_attnums : riinfo->fk_period_attnums; + Oid rangetype = rel_is_pk ? riinfo->pk_period_rangetype : riinfo->fk_period_rangetype; + Datum startvalue; + Datum endvalue; + Datum result; + bool startisnull; + bool endisnull; + LOCAL_FCINFO(fcinfo, 2); + FmgrInfo flinfo; + FuncExpr *f; + + InitFunctionCallInfoData(*fcinfo, &flinfo, 2, InvalidOid, NULL, NULL); + f = makeNode(FuncExpr); + f->funcresulttype = rangetype; + flinfo.fn_expr = (Node *) f; + flinfo.fn_extra = NULL; + + /* compute oldvalue */ + startvalue = slot_getattr(slot, period_attnums[0], &startisnull); + endvalue = slot_getattr(slot, period_attnums[1], &endisnull); + + fcinfo->args[0].value = startvalue; + fcinfo->args[0].isnull = startisnull; + fcinfo->args[1].value = endvalue; + fcinfo->args[1].isnull = endisnull; + + result = range_constructor2(fcinfo); + if (fcinfo->isnull) + elog(ERROR, "function %u returned NULL", flinfo.fn_oid); + + return result; +} + /* * RI_FKey_check - * @@ -348,6 +450,7 @@ RI_FKey_check(TriggerData *trigdata) StringInfoData querybuf; char pkrelname[MAX_QUOTED_REL_NAME_LEN]; char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; char paramname[16]; const char *querysep; Oid queryoids[RI_MAX_NUMKEYS]; @@ -355,35 +458,86 @@ RI_FKey_check(TriggerData *trigdata) /* ---------- * The query string built is - * SELECT 1 FROM [ONLY] x WHERE pkatt1 = $1 [AND ...] - * FOR KEY SHARE OF x + * SELECT 1 + * FROM [ONLY] x WHERE pkatt1 = $1 [AND ...] + * FOR KEY SHARE OF x * The type id's for the $ parameters are those of the * corresponding FK attributes. + * + * But for temporal FKs we need to make sure + * the FK's range is completely covered. + * So we use this query instead: + * SELECT 1 + * FROM ( + * SELECT pkperiodatt AS r + * FROM [ONLY] pktable x + * WHERE pkatt1 = $1 [AND ...] + * AND pkperiodatt && $n + * FOR KEY SHARE OF x + * ) x1 + * HAVING $n <@ range_agg(x1.r) + * Note if FOR KEY SHARE ever allows GROUP BY and HAVING + * we can make this a bit simpler. * ---------- */ initStringInfo(&querybuf); pk_only = pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ? "" : "ONLY "; quoteRelationName(pkrelname, pk_rel); - appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x", - pk_only, pkrelname); + if (riinfo->temporal) + { + if (riinfo->pk_period == InvalidOid) + { + quoteOneName(attname, + RIAttName(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1])); + attstr = attname; + } + else + attstr = query_period_range(riinfo, pk_rel, "x.", true); + + appendStringInfo(&querybuf, + "SELECT 1 FROM (SELECT %s AS r FROM %s%s x", + attstr, pk_only, pkrelname); + } + else { + appendStringInfo(&querybuf, "SELECT 1 FROM %s%s x", + pk_only, pkrelname); + } querysep = "WHERE"; for (int i = 0; i < riinfo->nkeys; i++) { - Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); - Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + Oid pk_type; + Oid fk_type; + + if (riinfo->pk_attnums[i] == InvalidOid) + { + pk_type = riinfo->pk_period_rangetype; + attstr = query_period_range(riinfo, pk_rel, "x.", true); + } + else + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + quoteOneName(attname, + RIAttName(pk_rel, riinfo->pk_attnums[i])); + attstr = attname; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + fk_type = riinfo->fk_period_rangetype; + else + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); - quoteOneName(attname, - RIAttName(pk_rel, riinfo->pk_attnums[i])); sprintf(paramname, "$%d", i + 1); ri_GenerateQual(&querybuf, querysep, - attname, pk_type, + attstr, pk_type, riinfo->pf_eq_oprs[i], paramname, fk_type); querysep = "AND"; queryoids[i] = fk_type; } appendStringInfoString(&querybuf, " FOR KEY SHARE OF x"); + if (riinfo->temporal) + appendStringInfo(&querybuf, ") x1 HAVING $%d <@ pg_catalog.range_agg(x1.r)", riinfo->nkeys); /* Prepare and save the plan */ qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids, @@ -400,6 +554,7 @@ RI_FKey_check(TriggerData *trigdata) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, NULL, newslot, + false, 0, pk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE, SPI_OK_SELECT); @@ -502,13 +657,24 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel, querysep = "WHERE"; for (int i = 0; i < riinfo->nkeys; i++) { - Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + Oid pk_type; + char *attstr; + if (riinfo->pk_attnums[i] == InvalidOid) + { + pk_type = riinfo->pk_period_rangetype; + attstr = query_period_range(riinfo, pk_rel, "x.", true); + } + else + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + quoteOneName(attname, + RIAttName(pk_rel, riinfo->pk_attnums[i])); + attstr = attname; + } - quoteOneName(attname, - RIAttName(pk_rel, riinfo->pk_attnums[i])); sprintf(paramname, "$%d", i + 1); ri_GenerateQual(&querybuf, querysep, - attname, pk_type, + attstr, pk_type, riinfo->pp_eq_oprs[i], paramname, pk_type); querysep = "AND"; @@ -527,6 +693,7 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel, result = ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, NULL, + false, 0, true, /* treat like update */ SPI_OK_SELECT); @@ -667,6 +834,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) StringInfoData querybuf; char fkrelname[MAX_QUOTED_REL_NAME_LEN]; char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; char paramname[16]; const char *querysep; Oid queryoids[RI_MAX_NUMKEYS]; @@ -689,18 +857,46 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) querysep = "WHERE"; for (int i = 0; i < riinfo->nkeys; i++) { - Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); - Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); - Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); - Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + Oid pk_type; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; - quoteOneName(attname, - RIAttName(fk_rel, riinfo->fk_attnums[i])); + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + attstr = query_period_range(riinfo, fk_rel, "x.", false); + else + { + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + attstr = attname; + } sprintf(paramname, "$%d", i + 1); ri_GenerateQual(&querybuf, querysep, paramname, pk_type, riinfo->pf_eq_oprs[i], - attname, fk_type); + attstr, fk_type); if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll)) ri_GenerateQualCollation(&querybuf, pk_coll); querysep = "AND"; @@ -719,6 +915,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, NULL, + false, 0, true, /* must detect new rows */ SPI_OK_SELECT); @@ -825,6 +1022,7 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, NULL, + false, 0, true, /* must detect new rows */ SPI_OK_DELETE); @@ -946,6 +1144,7 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, newslot, + false, 0, true, /* must detect new rows */ SPI_OK_UPDATE); @@ -1105,7 +1304,856 @@ ri_set(TriggerData *trigdata, bool is_set_null) ri_GenerateQual(&qualbuf, qualsep, paramname, pk_type, riinfo->pf_eq_oprs[i], - attname, fk_type); + attname, fk_type); + if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll)) + ri_GenerateQualCollation(&querybuf, pk_coll); + querysep = ","; + qualsep = "AND"; + queryoids[i] = pk_type; + } + appendBinaryStringInfo(&querybuf, qualbuf.data, qualbuf.len); + + /* Prepare and save the plan */ + qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids, + &qkey, fk_rel, pk_rel); + } + + /* + * We have a plan now. Run it to update the existing references. + */ + ri_PerformCheck(riinfo, &qkey, qplan, + fk_rel, pk_rel, + oldslot, NULL, + false, 0, + true, /* must detect new rows */ + SPI_OK_UPDATE); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish failed"); + + table_close(fk_rel, RowExclusiveLock); + + if (is_set_null) + return PointerGetDatum(NULL); + else + { + /* + * If we just deleted or updated the PK row whose key was equal to the + * FK columns' default values, and a referencing row exists in the FK + * table, we would have updated that row to the same values it already + * had --- and RI_FKey_fk_upd_check_required would hence believe no + * check is necessary. So we need to do another lookup now and in + * case a reference still exists, abort the operation. That is + * already implemented in the NO ACTION trigger, so just run it. (This + * recheck is only needed in the SET DEFAULT case, since CASCADE would + * remove such rows in case of a DELETE operation or would change the + * FK key values in case of an UPDATE, while SET NULL is certain to + * result in rows that satisfy the FK constraint.) + */ + return ri_restrict(trigdata, true); + } +} + + +/* + * RI_FKey_pk_upd_check_required - + * + * Check if we really need to fire the RI trigger for an update or delete to a PK + * relation. This is called by the AFTER trigger queue manager to see if + * it can skip queuing an instance of an RI trigger. Returns true if the + * trigger must be fired, false if we can prove the constraint will still + * be satisfied. + * + * newslot will be NULL if this is called for a delete. + */ +bool +RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel, + TupleTableSlot *oldslot, TupleTableSlot *newslot) +{ + const RI_ConstraintInfo *riinfo; + + riinfo = ri_FetchConstraintInfo(trigger, pk_rel, true); + + /* + * If any old key value is NULL, the row could not have been referenced by + * an FK row, so no check is needed. + */ + if (ri_NullCheck(RelationGetDescr(pk_rel), oldslot, riinfo, true) != RI_KEYS_NONE_NULL) + return false; + + /* If all old and new key values are equal, no check is needed */ + if (newslot && ri_KeysStable(pk_rel, oldslot, newslot, riinfo, true)) + return false; + + /* Else we need to fire the trigger. */ + return true; +} + +/* + * RI_FKey_fk_upd_check_required - + * + * Check if we really need to fire the RI trigger for an update to an FK + * relation. This is called by the AFTER trigger queue manager to see if + * it can skip queuing an instance of an RI trigger. Returns true if the + * trigger must be fired, false if we can prove the constraint will still + * be satisfied. + */ +bool +RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel, + TupleTableSlot *oldslot, TupleTableSlot *newslot) +{ + const RI_ConstraintInfo *riinfo; + int ri_nullcheck; + Datum xminDatum; + TransactionId xmin; + bool isnull; + + riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false); + + ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false); + + /* + * If all new key values are NULL, the row satisfies the constraint, so no + * check is needed. + */ + if (ri_nullcheck == RI_KEYS_ALL_NULL) + return false; + + /* + * If some new key values are NULL, the behavior depends on the match + * type. + */ + else if (ri_nullcheck == RI_KEYS_SOME_NULL) + { + switch (riinfo->confmatchtype) + { + case FKCONSTR_MATCH_SIMPLE: + + /* + * If any new key value is NULL, the row must satisfy the + * constraint, so no check is needed. + */ + return false; + + case FKCONSTR_MATCH_PARTIAL: + + /* + * Don't know, must run full check. + */ + break; + + case FKCONSTR_MATCH_FULL: + + /* + * If some new key values are NULL, the row fails the + * constraint. We must not throw error here, because the row + * might get invalidated before the constraint is to be + * checked, but we should queue the event to apply the check + * later. + */ + return true; + } + } + + /* + * Continues here for no new key values are NULL, or we couldn't decide + * yet. + */ + + /* + * If the original row was inserted by our own transaction, we must fire + * the trigger whether or not the keys are equal. This is because our + * UPDATE will invalidate the INSERT so that the INSERT RI trigger will + * not do anything; so we had better do the UPDATE check. (We could skip + * this if we knew the INSERT trigger already fired, but there is no easy + * way to know that.) + */ + xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull); + Assert(!isnull); + xmin = DatumGetTransactionId(xminDatum); + if (TransactionIdIsCurrentTransactionId(xmin)) + return true; + + /* If all old and new key values are equal, no check is needed */ + if (ri_KeysStable(fk_rel, oldslot, newslot, riinfo, false)) + return false; + + /* Else we need to fire the trigger. */ + return true; +} + +/* ---------- + * TRI_FKey_check_ins - + * + * Check temporal foreign key existence at insert event on FK table. + * ---------- + */ +Datum +TRI_FKey_check_ins(PG_FUNCTION_ARGS) +{ + /* + * Check that this is a valid trigger call on the right time and event. + */ + ri_CheckTrigger(fcinfo, "RI_FKey_check_ins", RI_TRIGTYPE_INSERT); + + /* + * Share code with UPDATE case. + */ + return RI_FKey_check((TriggerData *) fcinfo->context); +} + + +/* ---------- + * TRI_FKey_check_upd - + * + * Check temporal foreign key existence at update event on FK table. + * ---------- + */ +Datum +TRI_FKey_check_upd(PG_FUNCTION_ARGS) +{ + /* + * Check that this is a valid trigger call on the right time and event. + */ + ri_CheckTrigger(fcinfo, "RI_FKey_check_upd", RI_TRIGTYPE_UPDATE); + + /* + * Share code with INSERT case. + */ + return RI_FKey_check((TriggerData *) fcinfo->context); +} + + +/* ---------- + * TRI_FKey_noaction_del - + * + * Give an error and roll back the current transaction if the + * delete has resulted in a violation of the given temporal + * referential integrity constraint. + * ---------- + */ +Datum +TRI_FKey_noaction_del(PG_FUNCTION_ARGS) +{ + /* + * Check that this is a valid trigger call on the right time and event. + */ + ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_del", RI_TRIGTYPE_DELETE); + + /* + * Share code with RESTRICT/UPDATE cases. + */ + return ri_restrict((TriggerData *) fcinfo->context, true); +} + +/* + * TRI_FKey_restrict_del - + * + * Restrict delete from PK table to rows unreferenced by foreign key. + * + * The SQL standard intends that this referential action occur exactly when + * the delete is performed, rather than after. This appears to be + * the only difference between "NO ACTION" and "RESTRICT". In Postgres + * we still implement this as an AFTER trigger, but it's non-deferrable. + */ +Datum +TRI_FKey_restrict_del(PG_FUNCTION_ARGS) +{ + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_del", RI_TRIGTYPE_DELETE); + + /* Share code with NO ACTION/UPDATE cases. */ + return ri_restrict((TriggerData *) fcinfo->context, false); +} + +/* + * TRI_FKey_cascade_del - + * + * Cascaded delete foreign key references at delete event on temporal PK table. + */ +Datum +TRI_FKey_cascade_del(PG_FUNCTION_ARGS) +{ + TriggerData *trigdata = (TriggerData *) fcinfo->context; + const RI_ConstraintInfo *riinfo; + Relation fk_rel; + Relation pk_rel; + TupleTableSlot *oldslot; + RI_QueryKey qkey; + SPIPlanPtr qplan; + Datum targetRange; + + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_cascade_del", RI_TRIGTYPE_DELETE); + + riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger, + trigdata->tg_relation, true); + + /* + * Get the relation descriptors of the FK and PK tables and the old tuple. + * + * fk_rel is opened in RowExclusiveLock mode since that's what our + * eventual DELETE will get on it. + */ + fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock); + pk_rel = trigdata->tg_relation; + oldslot = trigdata->tg_trigslot; + + if (trigdata->tg_temporal) + targetRange = trigdata->tg_temporal->fp_targetRange; + else + targetRange = tupleRange(oldslot, riinfo); + + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + /* Fetch or prepare a saved plan for the cascaded delete */ + ri_BuildQueryKey(&qkey, riinfo, TRI_PLAN_CASCADE_DEL_DODELETE); + + if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL) + { + bool pkHasRange; + bool fkHasRange; + StringInfoData querybuf; + char fkrelname[MAX_QUOTED_REL_NAME_LEN]; + char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; + char paramname[16]; + const char *querysep; + Oid queryoids[RI_MAX_NUMKEYS]; + const char *fk_only; + + pkHasRange = riinfo->pk_period == InvalidOid; + fkHasRange = riinfo->fk_period == InvalidOid; + + /* ---------- + * The query string built is + * DELETE FROM [ONLY] + * FOR PORTION OF $fkatt FROM lower($n+1) TO upper($n+1) + * WHERE $1 = fkatt1 [AND ...] + * The type id's for the $ parameters are those of the + * corresponding PK attributes. + * ---------- + */ + initStringInfo(&querybuf); + fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ? + "" : "ONLY "; + quoteRelationName(fkrelname, fk_rel); + if (fkHasRange) + quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1])); + else + quoteOneName(attname, get_periodname(riinfo->fk_period, false)); + + // TODO: Better to find the greater of lower($n+1) and lower($1) + // and the lesser for upper($n+1) and upper($1), + // so we only delete what is being deleted from the pk table. + appendStringInfo(&querybuf, "DELETE FROM %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d)", + fk_only, fkrelname, attname, riinfo->nkeys + 1, riinfo->nkeys + 1); + querysep = "WHERE"; + for (int i = 0; i < riinfo->nkeys; i++) + { + Oid pk_type; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; + + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + attstr = query_period_range(riinfo, fk_rel, "", false); + else + { + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + attstr = attname; + } + sprintf(paramname, "$%d", i + 1); + ri_GenerateQual(&querybuf, querysep, + paramname, pk_type, + riinfo->pf_eq_oprs[i], + attstr, fk_type); + if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll)) + ri_GenerateQualCollation(&querybuf, pk_coll); + querysep = "AND"; + queryoids[i] = pk_type; + } + + if (pkHasRange) + queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]); + else + queryoids[riinfo->nkeys] = riinfo->pk_period_rangetype; + + /* Prepare and save the plan */ + qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys + 1, queryoids, + &qkey, fk_rel, pk_rel); + } + + /* + * We have a plan now. Build up the arguments from the key values in the + * deleted PK tuple and delete the referencing rows + */ + ri_PerformCheck(riinfo, &qkey, qplan, + fk_rel, pk_rel, + oldslot, NULL, + true, targetRange, + true, /* must detect new rows */ + SPI_OK_DELETE); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish failed"); + + table_close(fk_rel, RowExclusiveLock); + + return PointerGetDatum(NULL); +} + + +/* + * TRI_FKey_noaction_upd - + * + * Give an error and roll back the current transaction if the + * update has resulted in a violation of the given referential + * integrity constraint. + */ +Datum +TRI_FKey_noaction_upd(PG_FUNCTION_ARGS) +{ + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_noaction_upd", RI_TRIGTYPE_UPDATE); + + /* Share code with RESTRICT/DELETE cases. */ + return ri_restrict((TriggerData *) fcinfo->context, true); +} + +/* + * TRI_FKey_restrict_upd - + * + * Restrict update of PK to rows unreferenced by foreign key. + * + * The SQL standard intends that this referential action occur exactly when + * the update is performed, rather than after. This appears to be + * the only difference between "NO ACTION" and "RESTRICT". In Postgres + * we still implement this as an AFTER trigger, but it's non-deferrable. + */ +Datum +TRI_FKey_restrict_upd(PG_FUNCTION_ARGS) +{ + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_restrict_upd", RI_TRIGTYPE_UPDATE); + + /* Share code with NO ACTION/DELETE cases. */ + return ri_restrict((TriggerData *) fcinfo->context, false); +} + +/* + * TRI_FKey_cascade_upd - + * + * Cascaded update foreign key references at update event on temporal PK table. + */ +Datum +TRI_FKey_cascade_upd(PG_FUNCTION_ARGS) +{ + TriggerData *trigdata = (TriggerData *) fcinfo->context; + const RI_ConstraintInfo *riinfo; + Relation fk_rel; + Relation pk_rel; + TupleTableSlot *oldslot; + TupleTableSlot *newslot; + RI_QueryKey qkey; + SPIPlanPtr qplan; + Datum targetRange; + + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_cascade_upd", RI_TRIGTYPE_UPDATE); + + riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger, + trigdata->tg_relation, true); + + /* + * Get the relation descriptors of the FK and PK tables and the new and + * old tuple. + * + * fk_rel is opened in RowExclusiveLock mode since that's what our + * eventual UPDATE will get on it. + */ + fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock); + pk_rel = trigdata->tg_relation; + newslot = trigdata->tg_newslot; + oldslot = trigdata->tg_trigslot; + + if (trigdata->tg_temporal) + targetRange = trigdata->tg_temporal->fp_targetRange; + else + targetRange = tupleRange(oldslot, riinfo); + + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + /* Fetch or prepare a saved plan for the cascaded update */ + ri_BuildQueryKey(&qkey, riinfo, TRI_PLAN_CASCADE_UPD_DOUPDATE); + + if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL) + { + bool pkHasRange; + bool fkHasRange; + StringInfoData querybuf; + StringInfoData qualbuf; + char fkrelname[MAX_QUOTED_REL_NAME_LEN]; + char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; + char paramname[16]; + const char *querysep; + const char *qualsep; + Oid queryoids[RI_MAX_NUMKEYS]; + const char *fk_only; + + pkHasRange = riinfo->pk_period == InvalidOid; + fkHasRange = riinfo->fk_period == InvalidOid; + + /* ---------- + * The query string built is + * UPDATE [ONLY] + * FOR PORTION OF $fkatt FROM lower($n+1) TO upper($n+1) + * SET fkatt1 = $1, [, ...] + * WHERE $n = fkatt1 [AND ...] + * The type id's for the $ parameters are those of the + * corresponding PK attributes. Note that we are assuming + * there is an assignment cast from the PK to the FK type; + * else the parser will fail. + * ---------- + */ + initStringInfo(&querybuf); + initStringInfo(&qualbuf); + fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ? + "" : "ONLY "; + quoteRelationName(fkrelname, fk_rel); + if (fkHasRange) + quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1])); + else + quoteOneName(attname, get_periodname(riinfo->fk_period, false)); + + appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d) SET", + fk_only, fkrelname, attname, 2 * riinfo->nkeys + 1, 2 * riinfo->nkeys + 1); + + querysep = ""; + qualsep = "WHERE"; + for (int i = 0, j = riinfo->nkeys; i < riinfo->nkeys; i++, j++) + { + Oid pk_type; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; + + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + attstr = query_period_range(riinfo, fk_rel, "", false); + else + { + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + attstr = attname; + } + /* + * Don't set the temporal column(s). + * FOR PORTION OF will take care of that. + * TODO: I guess I could omit it from the qual too, right? And likewise with cascade deletes? + */ + if (i < riinfo->nkeys - 1) + appendStringInfo(&querybuf, + "%s %s = $%d", + querysep, attstr, i + 1); + sprintf(paramname, "$%d", j + 1); + ri_GenerateQual(&qualbuf, qualsep, + paramname, pk_type, + riinfo->pf_eq_oprs[i], + attstr, fk_type); + if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll)) + ri_GenerateQualCollation(&querybuf, pk_coll); + querysep = ","; + qualsep = "AND"; + queryoids[i] = pk_type; + queryoids[j] = pk_type; + } + appendBinaryStringInfo(&querybuf, qualbuf.data, qualbuf.len); + + if (pkHasRange) + queryoids[2 * riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]); + else + queryoids[2 * riinfo->nkeys] = riinfo->pk_period_rangetype; + + /* Prepare and save the plan */ + qplan = ri_PlanCheck(querybuf.data, 2 * riinfo->nkeys + 1, queryoids, + &qkey, fk_rel, pk_rel); + } + + /* + * We have a plan now. Build up the arguments from the key values in the + * updated PK tuple and update the referencing rows + */ + ri_PerformCheck(riinfo, &qkey, qplan, + fk_rel, pk_rel, + oldslot, newslot, + true, targetRange, + true, /* must detect new rows */ + SPI_OK_UPDATE); + + if (SPI_finish() != SPI_OK_FINISH) + elog(ERROR, "SPI_finish failed"); + + table_close(fk_rel, RowExclusiveLock); + + return PointerGetDatum(NULL); +} + +/* + * TRI_FKey_setnull_del - + * + * Set foreign key references to NULL values at delete event on PK table. + */ +Datum +TRI_FKey_setnull_del(PG_FUNCTION_ARGS) +{ + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_setnull_del", RI_TRIGTYPE_DELETE); + + /* Share code with UPDATE case */ + return tri_set((TriggerData *) fcinfo->context, true); +} + +/* + * TRI_FKey_setnull_upd - + * + * Set foreign key references to NULL at update event on PK table. + */ +Datum +TRI_FKey_setnull_upd(PG_FUNCTION_ARGS) +{ + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_setnull_upd", RI_TRIGTYPE_UPDATE); + + /* Share code with DELETE case */ + return tri_set((TriggerData *) fcinfo->context, true); +} + +/* + * TRI_FKey_setdefault_del - + * + * Set foreign key references to defaults at delete event on PK table. + */ +Datum +TRI_FKey_setdefault_del(PG_FUNCTION_ARGS) +{ + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_setdefault_del", RI_TRIGTYPE_DELETE); + + /* Share code with UPDATE case */ + return tri_set((TriggerData *) fcinfo->context, false); +} + +/* + * TRI_FKey_setdefault_upd - + * + * Set foreign key references to defaults at update event on PK table. + */ +Datum +TRI_FKey_setdefault_upd(PG_FUNCTION_ARGS) +{ + /* Check that this is a valid trigger call on the right time and event. */ + ri_CheckTrigger(fcinfo, "TRI_FKey_setdefault_upd", RI_TRIGTYPE_UPDATE); + + /* Share code with DELETE case */ + return tri_set((TriggerData *) fcinfo->context, false); +} + + +/* + * tri_set - + * + * Common code for temporal ON DELETE SET NULL, ON DELETE SET DEFAULT, ON + * UPDATE SET NULL, and ON UPDATE SET DEFAULT. + */ +static Datum +tri_set(TriggerData *trigdata, bool is_set_null) +{ + const RI_ConstraintInfo *riinfo; + Relation fk_rel; + Relation pk_rel; + TupleTableSlot *oldslot; + RI_QueryKey qkey; + SPIPlanPtr qplan; + Datum targetRange; + + riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger, + trigdata->tg_relation, true); + + /* + * Get the relation descriptors of the FK and PK tables and the old tuple. + * + * fk_rel is opened in RowExclusiveLock mode since that's what our + * eventual UPDATE will get on it. + */ + fk_rel = table_open(riinfo->fk_relid, RowExclusiveLock); + pk_rel = trigdata->tg_relation; + oldslot = trigdata->tg_trigslot; + + // TODO: It would be more correct to take the range from oldslot, + // and then take its intersection with the tg_temporal range (if any): + // Likewise for cascade_upd and cascade_del. + // But I don't think this does any harm: + if (trigdata->tg_temporal) + targetRange = trigdata->tg_temporal->fp_targetRange; + else + targetRange = tupleRange(oldslot, riinfo); + + if (SPI_connect() != SPI_OK_CONNECT) + elog(ERROR, "SPI_connect failed"); + + /* + * Fetch or prepare a saved plan for the set null/default operation (it's + * the same query for delete and update cases) + */ + ri_BuildQueryKey(&qkey, riinfo, + is_set_null + ? TRI_PLAN_SETNULL_DOUPDATE + : TRI_PLAN_SETDEFAULT_DOUPDATE); + + if ((qplan = ri_FetchPreparedPlan(&qkey)) == NULL) + { + bool pkHasRange; + bool fkHasRange; + StringInfoData querybuf; + StringInfoData qualbuf; + char fkrelname[MAX_QUOTED_REL_NAME_LEN]; + char attname[MAX_QUOTED_NAME_LEN]; + char *attstr; + char paramname[16]; + const char *querysep; + const char *qualsep; + Oid queryoids[RI_MAX_NUMKEYS]; + const char *fk_only; + + pkHasRange = riinfo->pk_period == InvalidOid; + fkHasRange = riinfo->fk_period == InvalidOid; + + /* ---------- + * The query string built is + * UPDATE [ONLY] + * FOR PORTION OF $fkatt FROM lower($n+1) TO upper($n+1) + * SET fkatt1 = {NULL|DEFAULT} [, ...] + * WHERE $1 = fkatt1 [AND ...] + * The type id's for the $ parameters are those of the + * corresponding PK attributes. + * ---------- + */ + initStringInfo(&querybuf); + initStringInfo(&qualbuf); + fk_only = fk_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE ? + "" : "ONLY "; + quoteRelationName(fkrelname, fk_rel); + if (fkHasRange) + quoteOneName(attname, RIAttName(fk_rel, riinfo->fk_attnums[riinfo->nkeys - 1])); + else + quoteOneName(attname, get_periodname(riinfo->fk_period, false)); + + appendStringInfo(&querybuf, "UPDATE %s%s FOR PORTION OF %s FROM lower($%d) TO upper($%d) SET", + fk_only, fkrelname, attname, riinfo->nkeys + 1, riinfo->nkeys + 1); + + querysep = ""; + qualsep = "WHERE"; + for (int i = 0; i < riinfo->nkeys; i++) + { + Oid pk_type; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; + + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + } + + if (riinfo->fk_attnums[i] == InvalidOid) + attstr = query_period_range(riinfo, fk_rel, "", false); + else + { + quoteOneName(attname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + attstr = attname; + } + + /* + * Don't set the temporal column(s). + * FOR PORTION OF will take care of that. + * TODO: I guess I could omit it from the qual too, right? And likewise with cascade deletes? + */ + if (i < riinfo->nkeys - 1) + appendStringInfo(&querybuf, + "%s %s = %s", + querysep, attstr, + is_set_null ? "NULL" : "DEFAULT"); + sprintf(paramname, "$%d", i + 1); + ri_GenerateQual(&qualbuf, qualsep, + paramname, pk_type, + riinfo->pf_eq_oprs[i], + attstr, fk_type); if (pk_coll != fk_coll && !get_collation_isdeterministic(pk_coll)) ri_GenerateQualCollation(&querybuf, pk_coll); querysep = ","; @@ -1114,8 +2162,13 @@ ri_set(TriggerData *trigdata, bool is_set_null) } appendBinaryStringInfo(&querybuf, qualbuf.data, qualbuf.len); + if (pkHasRange) + queryoids[riinfo->nkeys] = RIAttType(pk_rel, riinfo->pk_attnums[riinfo->nkeys - 1]); + else + queryoids[riinfo->nkeys] = riinfo->pk_period_rangetype; + /* Prepare and save the plan */ - qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys, queryoids, + qplan = ri_PlanCheck(querybuf.data, riinfo->nkeys + 1, queryoids, &qkey, fk_rel, pk_rel); } @@ -1125,6 +2178,7 @@ ri_set(TriggerData *trigdata, bool is_set_null) ri_PerformCheck(riinfo, &qkey, qplan, fk_rel, pk_rel, oldslot, NULL, + true, targetRange, true, /* must detect new rows */ SPI_OK_UPDATE); @@ -1155,132 +2209,6 @@ ri_set(TriggerData *trigdata, bool is_set_null) } -/* - * RI_FKey_pk_upd_check_required - - * - * Check if we really need to fire the RI trigger for an update or delete to a PK - * relation. This is called by the AFTER trigger queue manager to see if - * it can skip queuing an instance of an RI trigger. Returns true if the - * trigger must be fired, false if we can prove the constraint will still - * be satisfied. - * - * newslot will be NULL if this is called for a delete. - */ -bool -RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel, - TupleTableSlot *oldslot, TupleTableSlot *newslot) -{ - const RI_ConstraintInfo *riinfo; - - riinfo = ri_FetchConstraintInfo(trigger, pk_rel, true); - - /* - * If any old key value is NULL, the row could not have been referenced by - * an FK row, so no check is needed. - */ - if (ri_NullCheck(RelationGetDescr(pk_rel), oldslot, riinfo, true) != RI_KEYS_NONE_NULL) - return false; - - /* If all old and new key values are equal, no check is needed */ - if (newslot && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true)) - return false; - - /* Else we need to fire the trigger. */ - return true; -} - -/* - * RI_FKey_fk_upd_check_required - - * - * Check if we really need to fire the RI trigger for an update to an FK - * relation. This is called by the AFTER trigger queue manager to see if - * it can skip queuing an instance of an RI trigger. Returns true if the - * trigger must be fired, false if we can prove the constraint will still - * be satisfied. - */ -bool -RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel, - TupleTableSlot *oldslot, TupleTableSlot *newslot) -{ - const RI_ConstraintInfo *riinfo; - int ri_nullcheck; - Datum xminDatum; - TransactionId xmin; - bool isnull; - - riinfo = ri_FetchConstraintInfo(trigger, fk_rel, false); - - ri_nullcheck = ri_NullCheck(RelationGetDescr(fk_rel), newslot, riinfo, false); - - /* - * If all new key values are NULL, the row satisfies the constraint, so no - * check is needed. - */ - if (ri_nullcheck == RI_KEYS_ALL_NULL) - return false; - - /* - * If some new key values are NULL, the behavior depends on the match - * type. - */ - else if (ri_nullcheck == RI_KEYS_SOME_NULL) - { - switch (riinfo->confmatchtype) - { - case FKCONSTR_MATCH_SIMPLE: - - /* - * If any new key value is NULL, the row must satisfy the - * constraint, so no check is needed. - */ - return false; - - case FKCONSTR_MATCH_PARTIAL: - - /* - * Don't know, must run full check. - */ - break; - - case FKCONSTR_MATCH_FULL: - - /* - * If some new key values are NULL, the row fails the - * constraint. We must not throw error here, because the row - * might get invalidated before the constraint is to be - * checked, but we should queue the event to apply the check - * later. - */ - return true; - } - } - - /* - * Continues here for no new key values are NULL, or we couldn't decide - * yet. - */ - - /* - * If the original row was inserted by our own transaction, we must fire - * the trigger whether or not the keys are equal. This is because our - * UPDATE will invalidate the INSERT so that the INSERT RI trigger will - * not do anything; so we had better do the UPDATE check. (We could skip - * this if we knew the INSERT trigger already fired, but there is no easy - * way to know that.) - */ - xminDatum = slot_getsysattr(oldslot, MinTransactionIdAttributeNumber, &isnull); - Assert(!isnull); - xmin = DatumGetTransactionId(xminDatum); - if (TransactionIdIsCurrentTransactionId(xmin)) - return true; - - /* If all old and new key values are equal, no check is needed */ - if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false)) - return false; - - /* Else we need to fire the trigger. */ - return true; -} /* * RI_Initial_Check - @@ -1306,6 +2234,8 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel) StringInfoData querybuf; char pkrelname[MAX_QUOTED_REL_NAME_LEN]; char fkrelname[MAX_QUOTED_REL_NAME_LEN]; + char *pkattpart; + char *fkattpart; char pkattname[MAX_QUOTED_NAME_LEN + 3]; char fkattname[MAX_QUOTED_NAME_LEN + 3]; RangeTblEntry *pkrte; @@ -1387,10 +2317,24 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel) sep = ""; for (int i = 0; i < riinfo->nkeys; i++) { - quoteOneName(fkattname, - RIAttName(fk_rel, riinfo->fk_attnums[i])); - appendStringInfo(&querybuf, "%sfk.%s", sep, fkattname); - sep = ", "; + if (riinfo->fk_attnums[i] != InvalidOid) + { + quoteOneName(fkattname, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + appendStringInfo(&querybuf, "%sfk.%s", sep, fkattname); + sep = ", "; + } + else + { + quoteOneName(fkattname, + RIAttName(fk_rel, riinfo->fk_period_attnums[0])); + appendStringInfo(&querybuf, "%sfk.%s", sep, fkattname); + sep = ", "; + + quoteOneName(fkattname, + RIAttName(fk_rel, riinfo->fk_period_attnums[1])); + appendStringInfo(&querybuf, "%sfk.%s", sep, fkattname); + } } quoteRelationName(pkrelname, pk_rel); @@ -1408,19 +2352,47 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel) sep = "("; for (int i = 0; i < riinfo->nkeys; i++) { - Oid pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); - Oid fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); - Oid pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); - Oid fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + Oid pk_type; + Oid fk_type; + Oid pk_coll; + Oid fk_coll; + + if (riinfo->pk_attnums[i] != InvalidOid) + { + pk_type = RIAttType(pk_rel, riinfo->pk_attnums[i]); + pk_coll = RIAttCollation(pk_rel, riinfo->pk_attnums[i]); + + quoteOneName(pkattname + 3, + RIAttName(pk_rel, riinfo->pk_attnums[i])); + pkattpart = pkattname; + } + else + { + pk_type = riinfo->pk_period_rangetype; + pk_coll = riinfo->pk_period_collation; + pkattpart = query_period_range(riinfo, pk_rel, "pk.", true); + } + + if (riinfo->fk_attnums[i] != InvalidOid) + { + fk_type = RIAttType(fk_rel, riinfo->fk_attnums[i]); + fk_coll = RIAttCollation(fk_rel, riinfo->fk_attnums[i]); + + quoteOneName(fkattname + 3, + RIAttName(fk_rel, riinfo->fk_attnums[i])); + fkattpart = fkattname; + } + else + { + fk_type = riinfo->fk_period_rangetype; + fk_coll = riinfo->fk_period_collation; + fkattpart = query_period_range(riinfo, pk_rel, "fk.", false); + } - quoteOneName(pkattname + 3, - RIAttName(pk_rel, riinfo->pk_attnums[i])); - quoteOneName(fkattname + 3, - RIAttName(fk_rel, riinfo->fk_attnums[i])); ri_GenerateQual(&querybuf, sep, - pkattname, pk_type, + pkattpart, pk_type, riinfo->pf_eq_oprs[i], - fkattname, fk_type); + fkattpart, fk_type); if (pk_coll != fk_coll) ri_GenerateQualCollation(&querybuf, pk_coll); sep = "AND"; @@ -2034,6 +3006,43 @@ ri_FetchConstraintInfo(Trigger *trigger, Relation trig_rel, bool rel_is_pk) return riinfo; } +static void DeconstructFkConstraintPeriod(Oid periodid, Oid *rangetypeid, char *rangetypename, Oid *rngcollation, int16 *attnums) +{ + HeapTuple pertuple = SearchSysCache1(PERIODOID, periodid); + Form_pg_period period; + Form_pg_range rangetype; + Form_pg_type type; + HeapTuple rngtuple; + HeapTuple typtuple; + + if (!HeapTupleIsValid(pertuple)) + elog(ERROR, "cache lookup failed for period %d", periodid); + + period = (Form_pg_period) GETSTRUCT(pertuple); + + *rangetypeid = period->perrngtype; + attnums[0] = period->perstart; + attnums[1] = period->perend; + ReleaseSysCache(pertuple); + + rngtuple = SearchSysCache1(RANGETYPE, *rangetypeid); + if (!HeapTupleIsValid(rngtuple)) + elog(ERROR, "cache lookup failed for range %d", *rangetypeid); + + rangetype = (Form_pg_range) GETSTRUCT(rngtuple); + + *rngcollation = rangetype->rngcollation; + ReleaseSysCache(rngtuple); + + typtuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(*rangetypeid)); + if (!HeapTupleIsValid(typtuple)) + elog(ERROR, "cache lookup failed for type %u", *rangetypeid); + + type = (Form_pg_type) GETSTRUCT(typtuple); + strcpy(rangetypename, NameStr(type->typname)); + ReleaseSysCache(typtuple); +} + /* * Fetch or create the RI_ConstraintInfo struct for an FK constraint. */ @@ -2057,6 +3066,7 @@ ri_LoadConstraintInfo(Oid constraintOid) riinfo = (RI_ConstraintInfo *) hash_search(ri_constraint_cache, (void *) &constraintOid, HASH_ENTER, &found); + if (!found) riinfo->valid = false; else if (riinfo->valid) @@ -2091,6 +3101,24 @@ ri_LoadConstraintInfo(Oid constraintOid) riinfo->confupdtype = conForm->confupdtype; riinfo->confdeltype = conForm->confdeltype; riinfo->confmatchtype = conForm->confmatchtype; + riinfo->temporal = conForm->contemporal; + riinfo->pk_period = conForm->confperiod; + riinfo->pk_period_rangetype = InvalidOid; + riinfo->pk_period_collation = InvalidOid; + strcpy(riinfo->pk_period_rangetype_name, ""); + memset(riinfo->pk_period_attnums, 0, sizeof(riinfo->pk_period_attnums)); + riinfo->fk_period = conForm->conperiod; + riinfo->fk_period_rangetype = InvalidOid; + riinfo->fk_period_collation = InvalidOid; + strcpy(riinfo->fk_period_rangetype_name, ""); + memset(riinfo->fk_period_attnums, 0, sizeof(riinfo->fk_period_attnums)); + + if (conForm->confperiod != InvalidOid) + { + DeconstructFkConstraintPeriod(conForm->confperiod, &riinfo->pk_period_rangetype, riinfo->pk_period_rangetype_name, &riinfo->pk_period_collation, riinfo->pk_period_attnums); + } + if (conForm->conperiod != InvalidOid) + DeconstructFkConstraintPeriod(conForm->conperiod, &riinfo->fk_period_rangetype, riinfo->fk_period_rangetype_name, &riinfo->fk_period_collation, riinfo->fk_period_attnums); DeconstructFkConstraintRow(tup, &riinfo->nkeys, @@ -2242,6 +3270,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, RI_QueryKey *qkey, SPIPlanPtr qplan, Relation fk_rel, Relation pk_rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, + bool hasForPortionOf, Datum forPortionOf, bool detectNewRows, int expect_OK) { Relation query_rel, @@ -2287,15 +3316,16 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, if (newslot) { ri_ExtractValues(source_rel, newslot, riinfo, source_is_pk, - vals, nulls); + hasForPortionOf, forPortionOf, vals, nulls); if (oldslot) ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk, + hasForPortionOf, forPortionOf, vals + riinfo->nkeys, nulls + riinfo->nkeys); } else { ri_ExtractValues(source_rel, oldslot, riinfo, source_is_pk, - vals, nulls); + hasForPortionOf, forPortionOf, vals, nulls); } /* @@ -2377,6 +3407,7 @@ ri_PerformCheck(const RI_ConstraintInfo *riinfo, static void ri_ExtractValues(Relation rel, TupleTableSlot *slot, const RI_ConstraintInfo *riinfo, bool rel_is_pk, + bool hasForPortionOf, Datum forPortionOf, Datum *vals, char *nulls) { const int16 *attnums; @@ -2389,8 +3420,22 @@ ri_ExtractValues(Relation rel, TupleTableSlot *slot, for (int i = 0; i < riinfo->nkeys; i++) { - vals[i] = slot_getattr(slot, attnums[i], &isnull); - nulls[i] = isnull ? 'n' : ' '; + if (attnums[i] != InvalidOid) + { + vals[i] = slot_getattr(slot, attnums[i], &isnull); + nulls[i] = isnull ? 'n' : ' '; + } + else + { + vals[i] = build_period_range(riinfo, slot, rel_is_pk); + nulls[i] = ' '; + } + } + + if (hasForPortionOf) + { + vals[riinfo->nkeys] = forPortionOf; + nulls[riinfo->nkeys] = ' '; } } @@ -2413,6 +3458,7 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, StringInfoData key_values; bool onfk; const int16 *attnums; + const int16 *period_attnums; Oid rel_oid; AclResult aclresult; bool has_perm = true; @@ -2425,6 +3471,7 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, if (onfk) { attnums = riinfo->fk_attnums; + period_attnums = riinfo->fk_period_attnums; rel_oid = fk_rel->rd_id; if (tupdesc == NULL) tupdesc = fk_rel->rd_att; @@ -2432,6 +3479,7 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, else { attnums = riinfo->pk_attnums; + period_attnums = riinfo->pk_period_attnums; rel_oid = pk_rel->rd_id; if (tupdesc == NULL) tupdesc = pk_rel->rd_att; @@ -2484,12 +3532,18 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, for (int idx = 0; idx < riinfo->nkeys; idx++) { int fnum = attnums[idx]; - Form_pg_attribute att = TupleDescAttr(tupdesc, fnum - 1); + bool has_period = fnum == InvalidOid; char *name, *val; + Form_pg_attribute att; Datum datum; bool isnull; + if (has_period) + fnum = period_attnums[0]; + + att = TupleDescAttr(tupdesc, fnum - 1); + name = NameStr(att->attname); datum = slot_getattr(violatorslot, fnum, &isnull); @@ -2511,6 +3565,32 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, } appendStringInfoString(&key_names, name); appendStringInfoString(&key_values, val); + + if (has_period) + { + fnum = period_attnums[1]; + + att = TupleDescAttr(tupdesc, fnum - 1); + + name = NameStr(att->attname); + + datum = slot_getattr(violatorslot, fnum, &isnull); + if (!isnull) + { + Oid foutoid; + bool typisvarlena; + + getTypeOutputInfo(att->atttypid, &foutoid, &typisvarlena); + val = OidOutputFunctionCall(foutoid, datum); + } + else + val = "null"; + + appendStringInfoString(&key_names, ", "); + appendStringInfoString(&key_values, ", "); + appendStringInfoString(&key_names, name); + appendStringInfoString(&key_values, val); + } } } @@ -2577,10 +3657,21 @@ ri_NullCheck(TupleDesc tupDesc, for (int i = 0; i < riinfo->nkeys; i++) { - if (slot_attisnull(slot, attnums[i])) - nonenull = false; - else + if (attnums[i] == InvalidOid) + { + /* + * Never treat a period as null, because even if start and end + * are both null, that just signifies an unbounded range. + */ allnull = false; + } + else + { + if (slot_attisnull(slot, attnums[i])) + nonenull = false; + else + allnull = false; + } } if (allnull) @@ -2711,9 +3802,12 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan) /* - * ri_KeysEqual - + * ri_KeysStable - * - * Check if all key values in OLD and NEW are equal. + * Check if all key values in OLD and NEW are "equivalent": + * For normal FKs we check for equality. + * For temporal FKs we check that the PK side is a superset of its old value, + * or the FK side is a subset. * * Note: at some point we might wish to redefine this as checking for * "IS NOT DISTINCT" rather than "=", that is, allow two nulls to be @@ -2721,7 +3815,7 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan) * previously found at least one of the rows to contain no nulls. */ static bool -ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, +ri_KeysStable(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, const RI_ConstraintInfo *riinfo, bool rel_is_pk) { const int16 *attnums; @@ -2738,45 +3832,72 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, Datum newvalue; bool isnull; - /* - * Get one attribute's oldvalue. If it is NULL - they're not equal. - */ - oldvalue = slot_getattr(oldslot, attnums[i], &isnull); - if (isnull) - return false; - - /* - * Get one attribute's newvalue. If it is NULL - they're not equal. - */ - newvalue = slot_getattr(newslot, attnums[i], &isnull); - if (isnull) - return false; - - if (rel_is_pk) + if (riinfo->temporal && attnums[i] == InvalidOid) { /* - * If we are looking at the PK table, then do a bytewise - * comparison. We must propagate PK changes if the value is - * changed to one that "looks" different but would compare as - * equal using the equality operator. This only makes a - * difference for ON UPDATE CASCADE, but for consistency we treat - * all changes to the PK the same. + * We have a period, so we have to get the start/end columns + * and build a range. */ - Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1); - - if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen)) - return false; + oldvalue = build_period_range(riinfo, oldslot, rel_is_pk); + newvalue = build_period_range(riinfo, newslot, rel_is_pk); } else { /* - * For the FK table, compare with the appropriate equality - * operator. Changes that compare equal will still satisfy the - * constraint after the update. + * Get one attribute's oldvalue. If it is NULL - they're not equal. + */ + oldvalue = slot_getattr(oldslot, attnums[i], &isnull); + if (isnull) + return false; + + /* + * Get one attribute's newvalue. If it is NULL - they're not equal. */ - if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]), - oldvalue, newvalue)) + newvalue = slot_getattr(newslot, attnums[i], &isnull); + if (isnull) return false; + + } + + if (rel_is_pk) + { + if (riinfo->temporal && i == riinfo->nkeys - 1) + { + return DatumGetBool(DirectFunctionCall2(range_contains, newvalue, oldvalue)); + } + else + { + /* + * If we are looking at the PK table, then do a bytewise + * comparison. We must propagate PK changes if the value is + * changed to one that "looks" different but would compare as + * equal using the equality operator. This only makes a + * difference for ON UPDATE CASCADE, but for consistency we treat + * all changes to the PK the same. + */ + Form_pg_attribute att = TupleDescAttr(oldslot->tts_tupleDescriptor, attnums[i] - 1); + + if (!datum_image_eq(oldvalue, newvalue, att->attbyval, att->attlen)) + return false; + } + } + else + { + if (riinfo->temporal && i == riinfo->nkeys - 1) + { + return DatumGetBool(DirectFunctionCall2(range_contains, oldvalue, newvalue)); + } + else + { + /* + * For the FK table, compare with the appropriate equality + * operator. Changes that compare equal will still satisfy the + * constraint after the update. + */ + if (!ri_AttributesEqual(riinfo->ff_eq_oprs[i], RIAttType(rel, attnums[i]), + oldvalue, newvalue)) + return false; + } } } @@ -2950,3 +4071,17 @@ RI_FKey_trigger_type(Oid tgfoid) return RI_TRIGGER_NONE; } + +static Datum +tupleRange(TupleTableSlot *slot, const RI_ConstraintInfo *riinfo) +{ + bool isnull; + int16 attnum = riinfo->pk_attnums[riinfo->nkeys - 1]; + + if (attnum == InvalidOid) + /* Build a range from the PERIOD start and end columns */ + return build_period_range(riinfo, slot, true); + else + /* Get the range from the range column */ + return slot_getattr(slot, attnum, &isnull); +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index a768e4f55f..59ed0aaf09 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -333,7 +333,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, Oid indexId, - bool withoutOverlaps, Oid periodid, StringInfo buf); + bool withoutOverlaps, bool withPeriod, Oid periodid, 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, @@ -2196,7 +2196,12 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, elog(ERROR, "null conkey for constraint %u", constraintId); - decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, InvalidOid, &buf); + /* + * If it is a temporal foreign key + * then it uses PERIOD (which may be a real range column + * or may be a period). + */ + decompile_column_index_array(val, conForm->conrelid, conForm->conindid, false, conForm->contemporal, conForm->conperiod, &buf); /* add foreign relation name */ appendStringInfo(&buf, ") REFERENCES %s(", @@ -2210,7 +2215,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, elog(ERROR, "null confkey for constraint %u", constraintId); - decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, InvalidOid, &buf); + decompile_column_index_array(val, conForm->confrelid, conForm->conindid, false, conForm->contemporal, conForm->confperiod, &buf); appendStringInfoChar(&buf, ')'); @@ -2318,7 +2323,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, indexId = conForm->conindid; SysCacheGetAttr(CONSTROID, tup, Anum_pg_constraint_conexclop, &isnull); - keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, InvalidOid, &buf); + keyatts = decompile_column_index_array(val, conForm->conrelid, indexId, !isnull, false, InvalidOid, &buf); appendStringInfoChar(&buf, ')'); @@ -2518,7 +2523,7 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, */ static int decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId, - bool withoutOverlaps, Oid periodid, StringInfo buf) + bool withoutOverlaps, bool withPeriod, Oid periodid, StringInfo buf) { Datum *keys; int nKeys; @@ -2567,6 +2572,8 @@ decompile_column_index_array(Datum column_index_array, Oid relId, Oid indexId, appendStringInfoString(buf, quote_identifier(colName)); else if (withoutOverlaps && j == nKeys - 1) appendStringInfo(buf, ", %s WITHOUT OVERLAPS", quote_identifier(colName)); + else if (withPeriod && j == nKeys - 1) + appendStringInfo(buf, ", PERIOD %s", quote_identifier(colName)); else appendStringInfo(buf, ", %s", quote_identifier(colName)); } diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h index 6c6675b0f7..2babe42713 100644 --- a/src/include/catalog/pg_constraint.h +++ b/src/include/catalog/pg_constraint.h @@ -114,6 +114,7 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) bool contemporal; Oid conperiod; /* local PERIOD used in PK/FK constraint */ + Oid confperiod; /* referenced foreign PERIOD */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ @@ -129,19 +130,19 @@ CATALOG(pg_constraint,2606,ConstraintRelationId) int16 confkey[1]; /* - * If a foreign key, the OIDs of the PK = FK equality operators for each + * If a foreign key, the OIDs of the PK = FK comparison operators for each * column of the constraint */ Oid conpfeqop[1] BKI_LOOKUP(pg_operator); /* - * If a foreign key, the OIDs of the PK = PK equality operators for each + * If a foreign key, the OIDs of the PK = PK comparison operators for each * column of the constraint (i.e., equality for the referenced columns) */ Oid conppeqop[1] BKI_LOOKUP(pg_operator); /* - * If a foreign key, the OIDs of the FK = FK equality operators for each + * If a foreign key, the OIDs of the FK = FK comparison operators for each * column of the constraint (i.e., equality for the referencing columns) */ Oid conffeqop[1] BKI_LOOKUP(pg_operator); @@ -176,7 +177,7 @@ DECLARE_INDEX(pg_constraint_conparentid_index, 2579, ConstraintParentIndexId, on /* conkey can contain zero (InvalidAttrNumber) if a whole-row Var is used */ DECLARE_ARRAY_FOREIGN_KEY_OPT((conrelid, conkey), pg_attribute, (attrelid, attnum)); -DECLARE_ARRAY_FOREIGN_KEY((confrelid, confkey), pg_attribute, (attrelid, attnum)); +DECLARE_ARRAY_FOREIGN_KEY_OPT((confrelid, confkey), pg_attribute, (attrelid, attnum)); #ifdef EXPOSE_TO_CLIENT_CODE @@ -237,6 +238,7 @@ extern Oid CreateConstraintEntry(const char *constraintName, bool conNoInherit, bool conTemporal, Oid period, + Oid fperiod, bool is_internal); extern void RemoveConstraintById(Oid conId); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5537740256..3e503140eb 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3883,6 +3883,50 @@ prorettype => 'trigger', proargtypes => '', prosrc => 'RI_FKey_noaction_upd' }, +# Temporal referential integrity constraint triggers +{ oid => '6122', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES', + proname => 'TRI_FKey_check_ins', provolatile => 'v', prorettype => 'trigger', + proargtypes => '', prosrc => 'TRI_FKey_check_ins' }, +{ oid => '6123', descr => 'temporal referential integrity FOREIGN KEY ... REFERENCES', + proname => 'TRI_FKey_check_upd', provolatile => 'v', prorettype => 'trigger', + proargtypes => '', prosrc => 'TRI_FKey_check_upd' }, +{ oid => '6124', descr => 'temporal referential integrity ON DELETE CASCADE', + proname => 'TRI_FKey_cascade_del', provolatile => 'v', prorettype => 'trigger', + proargtypes => '', prosrc => 'TRI_FKey_cascade_del' }, +{ oid => '6125', descr => 'temporal referential integrity ON UPDATE CASCADE', + proname => 'TRI_FKey_cascade_upd', provolatile => 'v', prorettype => 'trigger', + proargtypes => '', prosrc => 'TRI_FKey_cascade_upd' }, +{ oid => '6126', descr => 'temporal referential integrity ON DELETE RESTRICT', + proname => 'TRI_FKey_restrict_del', provolatile => 'v', + prorettype => 'trigger', proargtypes => '', + prosrc => 'TRI_FKey_restrict_del' }, +{ oid => '6127', descr => 'temporal referential integrity ON UPDATE RESTRICT', + proname => 'TRI_FKey_restrict_upd', provolatile => 'v', + prorettype => 'trigger', proargtypes => '', + prosrc => 'TRI_FKey_restrict_upd' }, +{ oid => '6128', descr => 'temporal referential integrity ON DELETE SET NULL', + proname => 'TRI_FKey_setnull_del', provolatile => 'v', prorettype => 'trigger', + proargtypes => '', prosrc => 'TRI_FKey_setnull_del' }, +{ oid => '6129', descr => 'temporal referential integrity ON UPDATE SET NULL', + proname => 'TRI_FKey_setnull_upd', provolatile => 'v', prorettype => 'trigger', + proargtypes => '', prosrc => 'TRI_FKey_setnull_upd' }, +{ oid => '6130', descr => 'temporal referential integrity ON DELETE SET DEFAULT', + proname => 'TRI_FKey_setdefault_del', provolatile => 'v', + prorettype => 'trigger', proargtypes => '', + prosrc => 'TRI_FKey_setdefault_del' }, +{ oid => '6131', descr => 'temporal referential integrity ON UPDATE SET DEFAULT', + proname => 'TRI_FKey_setdefault_upd', provolatile => 'v', + prorettype => 'trigger', proargtypes => '', + prosrc => 'TRI_FKey_setdefault_upd' }, +{ oid => '6132', descr => 'temporal referential integrity ON DELETE NO ACTION', + proname => 'TRI_FKey_noaction_del', provolatile => 'v', + prorettype => 'trigger', proargtypes => '', + prosrc => 'TRI_FKey_noaction_del' }, +{ oid => '6133', descr => 'temporal referential integrity ON UPDATE NO ACTION', + proname => 'TRI_FKey_noaction_upd', provolatile => 'v', + prorettype => 'trigger', proargtypes => '', + prosrc => 'TRI_FKey_noaction_upd' }, + # Temporal leftovers triggers { oid => '8157', descr => 'temporal leftovers trigger function', proname => 'FP_insert_leftovers', provolatile => 'v', diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d2a59ca755..588f6407ae 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2338,7 +2338,9 @@ typedef struct Constraint /* Fields used for FOREIGN KEY constraints: */ RangeVar *pktable; /* Primary key table */ List *fk_attrs; /* Attributes of foreign key */ + Node *fk_period; /* String node naming Period or range column */ List *pk_attrs; /* Corresponding attrs in PK table */ + Node *pk_period; /* String node naming Period or range column */ char fk_matchtype; /* FULL, PARTIAL, SIMPLE */ char fk_upd_action; /* ON UPDATE action */ char fk_del_action; /* ON DELETE action */ diff --git a/src/include/utils/rangetypes.h b/src/include/utils/rangetypes.h index d8c1f1656a..fa18218bb2 100644 --- a/src/include/utils/rangetypes.h +++ b/src/include/utils/rangetypes.h @@ -150,5 +150,6 @@ extern bool range_split_internal(TypeCacheEntry *typcache, const RangeType *r1, extern void range_leftover_internal(TypeCacheEntry *typcache, const RangeType *r1, const RangeType *r2, RangeType **output1, RangeType **output2); +extern char *range_as_string(RangeType *r); #endif /* RANGETYPES_H */ diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index c70baad685..590d3f0c83 100644 --- a/src/test/regress/expected/for_portion_of.out +++ b/src/test/regress/expected/for_portion_of.out @@ -50,6 +50,15 @@ FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE SET valid_at = '[1990-01-01,1999-01-01)' WHERE id = '[5,6)'; ERROR: multiple assignments to same column "valid_at" +-- The wrong type fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM 1 TO 4 +SET name = 'nope' +WHERE id = '[3,4)'; +ERROR: function pg_catalog.tsrange(integer, integer) does not exist +LINE 2: FOR PORTION OF valid_at FROM 1 TO 4 + ^ +HINT: No function matches the given name and argument types. You might need to add explicit type casts. -- Setting with timestamps reversed fails UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out index 539e9688eb..069437b491 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -179,6 +179,7 @@ quad_poly_tbl|t radix_text_tbl|t ramp|f real_city|f +referencing_period_test|t reservations|f road|t shighway|t diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out index 708b5d3528..05fd62f4c3 100644 --- a/src/test/regress/expected/without_overlaps.out +++ b/src/test/regress/expected/without_overlaps.out @@ -284,3 +284,1849 @@ ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at TYPE tstzrange USING ts 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; +-- +-- test FK dependencies +-- +-- can't drop a range referenced by an FK, unless with CASCADE +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period2_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test2 (id, PERIOD valid_at) +); +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_at; +ERROR: cannot drop column valid_at of table without_overlaps_test2 because other objects depend on it +DETAIL: constraint referencing_period2_fk on table referencing_period_test2 depends on column valid_at of table without_overlaps_test2 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_at CASCADE; +NOTICE: drop cascades to constraint referencing_period2_fk on table referencing_period_test2 +DROP TABLE referencing_period_test2; +DROP TABLE without_overlaps_test2; +-- can't drop a PERIOD referenced by an FK, unless with CASCADE +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period2_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test2 (id, PERIOD valid_at) +); +ALTER TABLE without_overlaps_test2 DROP PERIOD FOR valid_at; +ERROR: cannot drop period valid_at on table without_overlaps_test2 because other objects depend on it +DETAIL: constraint referencing_period2_fk on table referencing_period_test2 depends on period valid_at on table without_overlaps_test2 +HINT: Use DROP ... CASCADE to drop the dependent objects too. +ALTER TABLE without_overlaps_test2 DROP PERIOD FOR valid_at CASCADE; +NOTICE: drop cascades to constraint referencing_period2_fk on table referencing_period_test2 +DROP TABLE referencing_period_test2; +DROP TABLE without_overlaps_test2; +-- +-- test FOREIGN KEY, range references range +-- +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at int4range, + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +ERROR: foreign key constraint "referencing_period_fk2" cannot be implemented +DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange. +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +DROP TABLE referencing_period_test; +-- with inferred PK on the referenced table: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test with rows already +-- +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "without_overlaps_test". +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + pg_get_constraintdef +------------------------------------------------------------------------------------------------ + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES without_overlaps_test(id, PERIOD valid_at) +(1 row) + +-- +-- test FK child inserts +-- +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "without_overlaps_test". +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03')); +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Tue May 01 00:00:00 2018")) is not present in table "without_overlaps_test". +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_at)=([8,9), ["Tue Jan 02 00:00:00 2018","Thu Mar 01 00:00:00 2018")) is not present in table "without_overlaps_test". +-- +-- test FK parent updates NO ACTION +-- +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-02-01', '2016-03-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent updates RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-02-01', '2016-03-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- +-- test FK parent deletes RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- +-- test ON UPDATE/DELETE options +-- +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8) + [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7) + [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7) +(3 rows) + +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8) + [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [7,8) + [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [7,8) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [10,11) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [16,17) + [10,11) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [15,16) +(2 rows) + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [8,9) + [5,6) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9) +(2 rows) + +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_at | parent_id +----+----------+----------- +(0 rows) + +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [11,12) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [17,18) +(1 row) + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | + [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [9,10) + [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [9,10) +(3 rows) + +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | + [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | + [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [12,13) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | + [12,13) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [18,19) +(2 rows) + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | + [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [11,12) + [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [11,12) +(3 rows) + +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | + [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | + [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [13,14) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | + [13,14) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [20,21) +(2 rows) + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', tsrange(null, null)); +INSERT INTO without_overlaps_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0) + [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [12,13) + [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [12,13) +(3 rows) + +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0) + [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0) + [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [14,15) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0) + [14,15) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [22,23) +(2 rows) + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_at | parent_id +--------+---------------------------------------------------------+----------- + [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0) + [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [14,15) + [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [14,15) +(3 rows) + +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_at | parent_id +--------+---------------------------------------------------------+----------- + [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0) + [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0) + [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [15,16) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0) + [15,16) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [24,25) +(2 rows) + +-- +-- test FOREIGN KEY, range references PERIOD +-- +DROP TABLE without_overlaps_test CASCADE; +NOTICE: drop cascades to constraint referencing_period_fk on table referencing_period_test +CREATE TABLE without_overlaps_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-01-02', '2018-02-03'); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-03-03', '2018-04-04'); +INSERT INTO without_overlaps_test VALUES ('[2,2]', '2018-01-01', '2018-01-05'); +INSERT INTO without_overlaps_test VALUES ('[3,3]', '2018-01-01', NULL); +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at int4range, + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +ERROR: foreign key constraint "referencing_period_fk2" cannot be implemented +DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange. +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test with rows already +-- +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "without_overlaps_test". +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + pg_get_constraintdef +------------------------------------------------------------------------------------------------ + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES without_overlaps_test(id, PERIOD valid_at) +(1 row) + +-- +-- test FK child inserts +-- +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Sun Apr 01 00:00:00 2018")) is not present in table "without_overlaps_test". +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-02-03', '2018-03-03'); +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_at)=([1,2), ["Tue Jan 02 00:00:00 2018","Tue May 01 00:00:00 2018")) is not present in table "without_overlaps_test". +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_at)=([8,9), ["Tue Jan 02 00:00:00 2018","Thu Mar 01 00:00:00 2018")) is not present in table "without_overlaps_test". +-- +-- test FK parent updates NO ACTION +-- +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent updates RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- +-- test FK parent deletes RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- +-- test ON UPDATE/DELETE options +-- +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8) + [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [6,7) + [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [6,7) +(3 rows) + +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [4,5) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [7,8) + [4,5) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [7,8) + [4,5) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [7,8) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [10,11) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [16,17) + [10,11) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [15,16) +(2 rows) + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [5,6) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [8,9) + [5,6) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [8,9) +(2 rows) + +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_at | parent_id +----+----------+----------- +(0 rows) + +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [11,12) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [17,18) +(1 row) + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | + [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [9,10) + [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [9,10) +(3 rows) + +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [6,7) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | + [6,7) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | + [6,7) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [12,13) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | + [12,13) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [18,19) +(2 rows) + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | + [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [11,12) + [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [11,12) +(3 rows) + +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [7,8) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | + [7,8) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | + [7,8) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [13,14) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | + [13,14) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [20,21) +(2 rows) + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', null, null); +INSERT INTO without_overlaps_test VALUES ('[12,12]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0) + [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [12,13) + [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [12,13) +(3 rows) + +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_at | parent_id +-------+---------------------------------------------------------+----------- + [8,9) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0) + [8,9) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0) + [8,9) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [14,15) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0) + [14,15) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [22,23) +(2 rows) + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_at | parent_id +--------+---------------------------------------------------------+----------- + [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0) + [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [14,15) + [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [14,15) +(3 rows) + +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_at | parent_id +--------+---------------------------------------------------------+----------- + [9,10) | ["Tue Jan 01 00:00:00 2019","Wed Jan 01 00:00:00 2020") | [-1,0) + [9,10) | ["Mon Jan 01 00:00:00 2018","Tue Jan 01 00:00:00 2019") | [-1,0) + [9,10) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [-1,0) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + id | valid_at | parent_id +---------+---------------------------------------------------------+----------- + [15,16) | ["Mon Jan 01 00:00:00 2018","Wed Jan 01 00:00:00 2020") | [-1,0) + [15,16) | ["Wed Jan 01 00:00:00 2020","Fri Jan 01 00:00:00 2021") | [24,25) +(2 rows) + +-- +-- test FOREIGN KEY, PERIOD references range +-- +DROP TABLE without_overlaps_test CASCADE; +NOTICE: drop cascades to constraint referencing_period_fk on table referencing_period_test +CREATE TABLE without_overlaps_test ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +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)); +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_from int4, + valid_til int4, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +ERROR: foreign key constraint "referencing_period_fk2" cannot be implemented +DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange. +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test with rows already +-- +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Sun Apr 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + pg_get_constraintdef +------------------------------------------------------------------------------------------------ + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES without_overlaps_test(id, PERIOD valid_at) +(1 row) + +-- +-- test FK child inserts +-- +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Sun Apr 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03')); +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Tue May 01 00:00:00 2018) is not present in table "without_overlaps_test". +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([8,9), Tue Jan 02 00:00:00 2018, Thu Mar 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- +-- test FK parent updates NO ACTION +-- +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-02-01', '2016-03-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01','2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent updates RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-02-01', '2016-03-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- +-- test FK parent deletes RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_at)=([5,6), ["Mon Jan 01 00:00:00 2018","Thu Feb 01 00:00:00 2018")) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- +-- test ON UPDATE/DELETE options +-- +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[4,4]', '2018-01-01', '2021-01-01', '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [4,5) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [7,8) + [4,5) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [6,7) + [4,5) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [6,7) +(3 rows) + +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [4,5) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [7,8) + [4,5) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [7,8) + [4,5) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [7,8) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[10,10]', '2018-01-01', '2021-01-01', '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [10,11) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | [16,17) + [10,11) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [15,16) +(2 rows) + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[5,5]', '2018-01-01', '2021-01-01', '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [5,6) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [8,9) + [5,6) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [8,9) +(2 rows) + +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_from | valid_til | parent_id +----+------------+-----------+----------- +(0 rows) + +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[11,11]', '2018-01-01', '2021-01-01', '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [11,12) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [17,18) +(1 row) + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[6,6]', '2018-01-01', '2021-01-01', '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [6,7) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | + [6,7) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [9,10) + [6,7) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [9,10) +(3 rows) + +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [6,7) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | + [6,7) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | + [6,7) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[12,12]', '2018-01-01', '2021-01-01', '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [12,13) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | + [12,13) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [18,19) +(2 rows) + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[7,7]', '2018-01-01', '2021-01-01', '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [7,8) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | + [7,8) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [11,12) + [7,8) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [11,12) +(3 rows) + +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [7,8) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | + [7,8) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | + [7,8) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[13,13]', '2018-01-01', '2021-01-01', '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [13,14) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | + [13,14) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [20,21) +(2 rows) + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', tsrange(null, null)); +INSERT INTO without_overlaps_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[8,8]', '2018-01-01', '2021-01-01', '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [8,9) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [-1,0) + [8,9) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [12,13) + [8,9) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [12,13) +(3 rows) + +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [8,9) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [-1,0) + [8,9) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [-1,0) + [8,9) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [-1,0) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[14,14]', '2018-01-01', '2021-01-01', '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [14,15) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | [-1,0) + [14,15) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [22,23) +(2 rows) + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[9,9]', '2018-01-01', '2021-01-01', '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_from | valid_til | parent_id +--------+--------------------------+--------------------------+----------- + [9,10) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [-1,0) + [9,10) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [14,15) + [9,10) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [14,15) +(3 rows) + +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_from | valid_til | parent_id +--------+--------------------------+--------------------------+----------- + [9,10) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [-1,0) + [9,10) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [-1,0) + [9,10) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [-1,0) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[15,15]', '2018-01-01', '2021-01-01', '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [15,16) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | [-1,0) + [15,16) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [24,25) +(2 rows) + +-- +-- test FOREIGN KEY, PERIOD references PERIOD +-- +DROP TABLE without_overlaps_test CASCADE; +NOTICE: drop cascades to constraint referencing_period_fk on table referencing_period_test +CREATE TABLE without_overlaps_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-01-02', '2018-02-03'); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-03-03', '2018-04-04'); +INSERT INTO without_overlaps_test VALUES ('[2,2]', '2018-01-01', '2018-01-05'); +INSERT INTO without_overlaps_test VALUES ('[3,3]', '2018-01-01', NULL); +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_from int4, + valid_til int4, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +ERROR: foreign key constraint "referencing_period_fk2" cannot be implemented +DETAIL: Key columns "valid_at" and "valid_at" are of incompatible types: int4range and tsrange. +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test ALTER TABLE ADD CONSTRAINT +-- +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); +ERROR: foreign key referenced-columns list must not contain duplicates +-- +-- test with rows already +-- +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Sun Apr 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- +-- test pg_get_constraintdef +-- +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + pg_get_constraintdef +------------------------------------------------------------------------------------------------ + FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES without_overlaps_test(id, PERIOD valid_at) +(1 row) + +-- +-- test FK child inserts +-- +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Sun Apr 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-02-03', '2018-03-03'); +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([1,2), Tue Jan 02 00:00:00 2018, Tue May 01 00:00:00 2018) is not present in table "without_overlaps_test". +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; +ERROR: insert or update on table "referencing_period_test" violates foreign key constraint "referencing_period_fk" +DETAIL: Key (parent_id, valid_from, valid_til)=([8,9), Tue Jan 02 00:00:00 2018, Thu Mar 01 00:00:00 2018) is not present in table "without_overlaps_test". +-- +-- test FK parent updates NO ACTION +-- +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent updates RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- +-- test FK parent deletes RESTRICT +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +ERROR: update or delete on table "without_overlaps_test" violates foreign key constraint "referencing_period_fk" on table "referencing_period_test" +DETAIL: Key (id, valid_from, valid_til)=([5,6), Mon Jan 01 00:00:00 2018, Thu Feb 01 00:00:00 2018) is still referenced from table "referencing_period_test". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- +-- test ON UPDATE/DELETE options +-- +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[4,4]', '2018-01-01', '2021-01-01', '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [4,5) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [7,8) + [4,5) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [6,7) + [4,5) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [6,7) +(3 rows) + +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [4,5) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [7,8) + [4,5) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [7,8) + [4,5) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [7,8) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[10,10]', '2018-01-01', '2021-01-01', '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [10,11) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | [16,17) + [10,11) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [15,16) +(2 rows) + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[5,5]', '2018-01-01', '2021-01-01', '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [5,6) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [8,9) + [5,6) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [8,9) +(2 rows) + +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; + id | valid_from | valid_til | parent_id +----+------------+-----------+----------- +(0 rows) + +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[11,11]', '2018-01-01', '2021-01-01', '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [11,12) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [17,18) +(1 row) + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[6,6]', '2018-01-01', '2021-01-01', '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [6,7) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | + [6,7) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [9,10) + [6,7) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [9,10) +(3 rows) + +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [6,7) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | + [6,7) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | + [6,7) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[12,12]', '2018-01-01', '2021-01-01', '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [12,13) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | + [12,13) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [18,19) +(2 rows) + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[7,7]', '2018-01-01', '2021-01-01', '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [7,8) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | + [7,8) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [11,12) + [7,8) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [11,12) +(3 rows) + +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [7,8) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | + [7,8) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | + [7,8) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[13,13]', '2018-01-01', '2021-01-01', '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [13,14) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | + [13,14) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [20,21) +(2 rows) + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', null, null); +INSERT INTO without_overlaps_test VALUES ('[12,12]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[8,8]', '2018-01-01', '2021-01-01', '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [8,9) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [-1,0) + [8,9) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [12,13) + [8,9) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [12,13) +(3 rows) + +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; + id | valid_from | valid_til | parent_id +-------+--------------------------+--------------------------+----------- + [8,9) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [-1,0) + [8,9) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [-1,0) + [8,9) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [-1,0) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[14,14]', '2018-01-01', '2021-01-01', '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [14,15) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | [-1,0) + [14,15) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [22,23) +(2 rows) + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[9,9]', '2018-01-01', '2021-01-01', '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_from | valid_til | parent_id +--------+--------------------------+--------------------------+----------- + [9,10) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [-1,0) + [9,10) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [14,15) + [9,10) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [14,15) +(3 rows) + +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; + id | valid_from | valid_til | parent_id +--------+--------------------------+--------------------------+----------- + [9,10) | Tue Jan 01 00:00:00 2019 | Wed Jan 01 00:00:00 2020 | [-1,0) + [9,10) | Mon Jan 01 00:00:00 2018 | Tue Jan 01 00:00:00 2019 | [-1,0) + [9,10) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [-1,0) +(3 rows) + +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[15,15]', '2018-01-01', '2021-01-01', '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + id | valid_from | valid_til | parent_id +---------+--------------------------+--------------------------+----------- + [15,16) | Mon Jan 01 00:00:00 2018 | Wed Jan 01 00:00:00 2020 | [-1,0) + [15,16) | Wed Jan 01 00:00:00 2020 | Fri Jan 01 00:00:00 2021 | [24,25) +(2 rows) + diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index 3b5755ba00..b773ab7a08 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -48,6 +48,12 @@ FOR PORTION OF valid_at FROM '2018-06-01' TO MAXVALUE SET valid_at = '[1990-01-01,1999-01-01)' WHERE id = '[5,6)'; +-- The wrong type fails +UPDATE for_portion_of_test +FOR PORTION OF valid_at FROM 1 TO 4 +SET name = 'nope' +WHERE id = '[3,4)'; + -- Setting with timestamps reversed fails UPDATE for_portion_of_test FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-01-01' diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql index e64ec69f0b..5ade1b8079 100644 --- a/src/test/regress/sql/without_overlaps.sql +++ b/src/test/regress/sql/without_overlaps.sql @@ -293,3 +293,1405 @@ ALTER TABLE without_overlaps_test2 ALTER COLUMN valid_at TYPE tstzrange USING ts 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; + +-- +-- test FK dependencies +-- + +-- can't drop a range referenced by an FK, unless with CASCADE +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period2_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test2 (id, PERIOD valid_at) +); +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_at; +ALTER TABLE without_overlaps_test2 DROP COLUMN valid_at CASCADE; +DROP TABLE referencing_period_test2; +DROP TABLE without_overlaps_test2; + +-- can't drop a PERIOD referenced by an FK, unless with CASCADE +CREATE TABLE without_overlaps_test2 ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period2_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period2_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test2 (id, PERIOD valid_at) +); +ALTER TABLE without_overlaps_test2 DROP PERIOD FOR valid_at; +ALTER TABLE without_overlaps_test2 DROP PERIOD FOR valid_at CASCADE; +DROP TABLE referencing_period_test2; +DROP TABLE without_overlaps_test2; + +-- +-- test FOREIGN KEY, range references range +-- + +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at int4range, + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); + +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); +DROP TABLE referencing_period_test; + +-- with inferred PK on the referenced table: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; + +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); + +-- +-- test with rows already +-- + +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + +-- +-- test FK child inserts +-- + +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03')); +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); + +-- +-- test FK child updates +-- + +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]'; +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; + +-- +-- test FK parent updates NO ACTION +-- + +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-02-01', '2016-03-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent updates RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-02-01', '2016-03-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- +-- test FK parent deletes NO ACTION +-- +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); + +-- +-- test FK parent deletes RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); + +-- +-- test ON UPDATE/DELETE options +-- + +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', tsrange(null, null)); +INSERT INTO without_overlaps_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + + + +-- +-- test FOREIGN KEY, range references PERIOD +-- + +DROP TABLE without_overlaps_test CASCADE; +CREATE TABLE without_overlaps_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-01-02', '2018-02-03'); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-03-03', '2018-04-04'); +INSERT INTO without_overlaps_test VALUES ('[2,2]', '2018-01-01', '2018-01-05'); +INSERT INTO without_overlaps_test VALUES ('[3,3]', '2018-01-01', NULL); + +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_at int4range, + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); + +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; + +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +CREATE TABLE referencing_period_test ( + id int4range, + valid_at tsrange, + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); + +-- +-- test with rows already +-- + +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + +-- +-- test FK child inserts +-- + +INSERT INTO referencing_period_test VALUES ('[1,1]', tsrange('2018-01-02', '2018-02-01'), '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-02-03', '2018-03-03'); +INSERT INTO referencing_period_test VALUES ('[2,2]', tsrange('2018-01-02', '2018-04-01'), '[1,1]'); + +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-03-01') WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_at = tsrange('2018-01-02', '2018-05-01') WHERE id = '[1,1]'; +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; + +-- +-- test FK parent updates NO ACTION +-- + +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent updates RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent deletes NO ACTION +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; + +-- +-- test FK parent deletes RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', tsrange('2018-01-05', '2018-01-10'), '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; + +-- +-- test ON UPDATE/DELETE options +-- + +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[4,4]', tsrange('2018-01-01', '2021-01-01'), '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[10,10]', tsrange('2018-01-01', '2021-01-01'), '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[5,5]', tsrange('2018-01-01', '2021-01-01'), '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01'), '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01'), '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01'), '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[7,7]', tsrange('2018-01-01', '2021-01-01'), '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[13,13]', tsrange('2018-01-01', '2021-01-01'), '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', null, null); +INSERT INTO without_overlaps_test VALUES ('[12,12]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01'), '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01'), '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01'), '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[15,15]', tsrange('2018-01-01', '2021-01-01'), '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + + +-- +-- test FOREIGN KEY, PERIOD references range +-- + +DROP TABLE without_overlaps_test CASCADE; +CREATE TABLE without_overlaps_test ( + id int4range, + valid_at tsrange, + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +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)); + +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_from int4, + valid_til int4, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); + +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; + +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); + +-- +-- test with rows already +-- + +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + +-- +-- test FK child inserts +-- + +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', tsrange('2018-02-03', '2018-03-03')); +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); + +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]'; +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; + +-- +-- test FK parent updates NO ACTION +-- + +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-02-01', '2016-03-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01','2018-02-01'); +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent updates RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_at = tsrange('2016-02-01', '2016-03-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_at = tsrange('2016-01-01', '2016-02-01') +WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent deletes NO ACTION +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); + +-- +-- test FK parent deletes RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-01-01', '2018-02-01')); +INSERT INTO without_overlaps_test VALUES ('[5,5]', tsrange('2018-02-01', '2018-03-01')); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_at = tsrange('2018-01-01', '2018-02-01'); + +-- +-- test ON UPDATE/DELETE options +-- + +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[4,4]', '2018-01-01', '2021-01-01', '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[15,15]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[10,10]', '2018-01-01', '2021-01-01', '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[5,5]', '2018-01-01', '2021-01-01', '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[17,17]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[11,11]', '2018-01-01', '2021-01-01', '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[6,6]', '2018-01-01', '2021-01-01', '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[18,18]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[12,12]', '2018-01-01', '2021-01-01', '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[7,7]', '2018-01-01', '2021-01-01', '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[20,20]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[13,13]', '2018-01-01', '2021-01-01', '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', tsrange(null, null)); +INSERT INTO without_overlaps_test VALUES ('[12,12]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[8,8]', '2018-01-01', '2021-01-01', '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[22,22]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[14,14]', '2018-01-01', '2021-01-01', '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', tsrange('2018-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[9,9]', '2018-01-01', '2021-01-01', '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2018-01-01', '2020-01-01')); +INSERT INTO without_overlaps_test VALUES ('[24,24]', tsrange('2020-01-01', '2021-01-01')); +INSERT INTO referencing_period_test VALUES ('[15,15]', '2018-01-01', '2021-01-01', '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND valid_at @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; + + +-- +-- test FOREIGN KEY, PERIOD references PERIOD +-- + +DROP TABLE without_overlaps_test CASCADE; +CREATE TABLE without_overlaps_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + CONSTRAINT without_overlaps_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-01-02', '2018-02-03'); +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-03-03', '2018-04-04'); +INSERT INTO without_overlaps_test VALUES ('[2,2]', '2018-01-01', '2018-01-05'); +INSERT INTO without_overlaps_test VALUES ('[3,3]', '2018-01-01', NULL); + +-- Can't create a FK with a mismatched range type +CREATE TABLE referencing_period_test2 ( + id int4range, + valid_from int4, + valid_til int4, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk2 PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk2 FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at) +); + +-- with inferred PK on the referenced table: +DROP TABLE referencing_period_test; +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test +); +DROP TABLE referencing_period_test; + +-- should fail because of duplicate referenced columns: +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS), + CONSTRAINT referencing_period_fk FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id) +); + +-- +-- test ALTER TABLE ADD CONSTRAINT +-- + +CREATE TABLE referencing_period_test ( + id int4range, + valid_from timestamp, + valid_til timestamp, + PERIOD FOR valid_at (valid_from, valid_til), + parent_id int4range, + CONSTRAINT referencing_period_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS) +); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test (id, PERIOD valid_at); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +-- with inferred PK on the referenced table: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- should fail because of duplicate referenced columns: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk2 + FOREIGN KEY (parent_id, PERIOD parent_id) + REFERENCES without_overlaps_test (id, PERIOD id); + +-- +-- test with rows already +-- + +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- should fail: +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- okay again: +DELETE FROM referencing_period_test; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; + +-- +-- test pg_get_constraintdef +-- + +SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = 'referencing_period_fk'; + +-- +-- test FK child inserts +-- + +INSERT INTO referencing_period_test VALUES ('[1,1]', '2018-01-02', '2018-02-01', '[1,1]'); +-- should fail: +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); +-- now it should work: +INSERT INTO without_overlaps_test VALUES ('[1,1]', '2018-02-03', '2018-03-03'); +INSERT INTO referencing_period_test VALUES ('[2,2]', '2018-01-02', '2018-04-01', '[1,1]'); + +-- +-- test FK child updates +-- +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-03-01' WHERE id = '[1,1]'; +-- should fail: +UPDATE referencing_period_test SET valid_from = '2018-01-02', valid_til = '2018-05-01' WHERE id = '[1,1]'; +UPDATE referencing_period_test SET parent_id = '[8,8]' WHERE id = '[1,1]'; + +-- +-- test FK parent updates NO ACTION +-- + +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent updates RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' WHERE id = '[5,5]'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +UPDATE without_overlaps_test SET valid_from = '2016-02-01', valid_til = '2016-03-01' +WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK update that fails because both are referenced: +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +UPDATE without_overlaps_test SET valid_from = '2016-01-01', valid_til = '2016-02-01' +WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- clean up: +DELETE FROM referencing_period_test WHERE parent_id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; + +-- +-- test FK parent deletes NO ACTION +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; + +-- +-- test FK parent deletes RESTRICT +-- + +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk; +ALTER TABLE referencing_period_test + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE RESTRICT; +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-01-01', '2018-02-01'); +INSERT INTO without_overlaps_test VALUES ('[5,5]', '2018-02-01', '2018-03-01'); +INSERT INTO referencing_period_test VALUES ('[3,3]', '2018-01-05', '2018-01-10', '[5,5]'); +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-02-01' AND valid_til = '2018-03-01'; +-- a PK delete that fails because both are referenced: +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM referencing_period_test WHERE id = '[3,3]'; +DELETE FROM without_overlaps_test WHERE id = '[5,5]' AND valid_from = '2018-01-01' AND valid_til = '2018-02-01'; + +-- +-- test ON UPDATE/DELETE options +-- + +-- test FK parent updates CASCADE +INSERT INTO without_overlaps_test VALUES ('[6,6]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[4,4]', '2018-01-01', '2021-01-01', '[6,6]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE CASCADE ON UPDATE CASCADE; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +UPDATE without_overlaps_test SET id = '[7,7]' WHERE id = '[6,6]'; +SELECT * FROM referencing_period_test WHERE id = '[4,4]'; +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[15,15]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[10,10]', '2018-01-01', '2021-01-01', '[15,15]'); +UPDATE without_overlaps_test SET id = '[16,16]' WHERE id = '[15,15]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[10,10]'; + +-- test FK parent deletes CASCADE +INSERT INTO without_overlaps_test VALUES ('[8,8]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[5,5]', '2018-01-01', '2021-01-01', '[8,8]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +DELETE FROM without_overlaps_test WHERE id = '[8,8]'; +SELECT * FROM referencing_period_test WHERE id = '[5,5]'; +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[17,17]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[11,11]', '2018-01-01', '2021-01-01', '[17,17]'); +DELETE FROM without_overlaps_test WHERE id = '[17,17]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[11,11]'; + +-- test FK parent updates SET NULL +INSERT INTO without_overlaps_test VALUES ('[9,9]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[6,6]', '2018-01-01', '2021-01-01', '[9,9]'); +ALTER TABLE referencing_period_test + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET NULL ON UPDATE SET NULL; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +UPDATE without_overlaps_test SET id = '[10,10]' WHERE id = '[9,9]'; +SELECT * FROM referencing_period_test WHERE id = '[6,6]'; +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[18,18]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[12,12]', '2018-01-01', '2021-01-01', '[18,18]'); +UPDATE without_overlaps_test SET id = '[19,19]' WHERE id = '[18,18]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[12,12]'; + +-- test FK parent deletes SET NULL +INSERT INTO without_overlaps_test VALUES ('[11,11]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[7,7]', '2018-01-01', '2021-01-01', '[11,11]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +DELETE FROM without_overlaps_test WHERE id = '[11,11]'; +SELECT * FROM referencing_period_test WHERE id = '[7,7]'; +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[20,20]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[13,13]', '2018-01-01', '2021-01-01', '[20,20]'); +DELETE FROM without_overlaps_test WHERE id = '[20,20]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[13,13]'; + +-- test FK parent updates SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[-1,-1]', null, null); +INSERT INTO without_overlaps_test VALUES ('[12,12]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[8,8]', '2018-01-01', '2021-01-01', '[12,12]'); +ALTER TABLE referencing_period_test + ALTER COLUMN parent_id SET DEFAULT '[-1,-1]', + DROP CONSTRAINT referencing_period_fk, + ADD CONSTRAINT referencing_period_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES without_overlaps_test + ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; +UPDATE without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +UPDATE without_overlaps_test SET id = '[13,13]' WHERE id = '[12,12]'; +SELECT * FROM referencing_period_test WHERE id = '[8,8]'; +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[22,22]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[14,14]', '2018-01-01', '2021-01-01', '[22,22]'); +UPDATE without_overlaps_test SET id = '[23,23]' WHERE id = '[22,22]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[14,14]'; + +-- test FK parent deletes SET DEFAULT +INSERT INTO without_overlaps_test VALUES ('[14,14]', '2018-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[9,9]', '2018-01-01', '2021-01-01', '[14,14]'); +DELETE FROM without_overlaps_test FOR PORTION OF valid_at FROM '2019-01-01' TO '2020-01-01' WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +DELETE FROM without_overlaps_test WHERE id = '[14,14]'; +SELECT * FROM referencing_period_test WHERE id = '[9,9]'; +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2018-01-01', '2020-01-01'); +INSERT INTO without_overlaps_test VALUES ('[24,24]', '2020-01-01', '2021-01-01'); +INSERT INTO referencing_period_test VALUES ('[15,15]', '2018-01-01', '2021-01-01', '[24,24]'); +DELETE FROM without_overlaps_test WHERE id = '[24,24]' AND tsrange(valid_from, valid_til) @> '2019-01-01'::timestamp; +SELECT * FROM referencing_period_test WHERE id = '[15,15]'; -- 2.25.1 --------------AA1A9756402C5229DC7A97A9--