From c4284295c93c92d2b4ca9c1ac7f9c2b741032f38 Mon Sep 17 00:00:00 2001 From: "Paul A. Jungwirth" Date: Wed, 12 Aug 2026 21:41:04 -0700 Subject: [PATCH v1 5/7] Add RESTRICT for temporal foreign keys A RESTRICT foreign key must fail if the reference changes, even if another record replaces the reference. You cannot defer it. But for temporal foreign keys, "the reference" refers to individual instants, not the whole span of history held by the referenced tuple (see the use of EXPAND and UNNEST in 11.8 Syntax Rules of SQL:2011 through SQL:2023). To avoid including separately-added records, we compute which parts of history were lost and search for references to them. The without_portion helper functions used by FOR PORTION OF are exactly what we need, so we use those here as well. Author: Paul A. Jungwirth --- doc/src/sgml/ddl.sgml | 6 +- doc/src/sgml/ref/create_table.sgml | 7 +- src/backend/commands/tablecmds.c | 6 +- src/backend/utils/adt/ri_triggers.c | 319 +++++++++++++++-- .../regress/expected/without_overlaps.out | 333 +++++++++++++++++- src/test/regress/sql/without_overlaps.sql | 245 +++++++++++++ src/tools/pgindent/typedefs.list | 1 + 7 files changed, 871 insertions(+), 46 deletions(-) diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index 23299346e18..8fe6a340277 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1861,9 +1861,9 @@ CREATE TABLE variants ( PostgreSQL supports temporal foreign keys with - action NO ACTION, but not RESTRICT, - CASCADE, SET NULL, or SET - DEFAULT. + action NO ACTION and RESTRICT, but + not CASCADE, SET NULL, or + SET DEFAULT. diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index e342585c7f0..9c73850e652 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -1321,7 +1321,12 @@ WITH ( MODULUS numeric_literal, REM - In a temporal foreign key, this option is not supported. + In a temporal foreign key, only the portion of the referenced + row's application time that is actually changed/removed is checked, not + its entire period. An update or delete that leaves the referenced + instants intact is therefore allowed, for example an update that grows + the period or a delete that shrinks it but without losing any + referenced instants. diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 1baa6c05718..fef76ed9ee3 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -10374,8 +10374,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, */ if (fkconstraint->fk_with_period) { - if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_RESTRICT || - fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE || + if (fkconstraint->fk_upd_action == FKCONSTR_ACTION_CASCADE || fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETNULL || fkconstraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT) ereport(ERROR, @@ -10383,8 +10382,7 @@ ATAddForeignKeyConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel, errmsg("unsupported %s action for foreign key constraint using PERIOD", "ON UPDATE")); - if (fkconstraint->fk_del_action == FKCONSTR_ACTION_RESTRICT || - fkconstraint->fk_del_action == FKCONSTR_ACTION_CASCADE || + if (fkconstraint->fk_del_action == FKCONSTR_ACTION_CASCADE || fkconstraint->fk_del_action == FKCONSTR_ACTION_SETNULL || fkconstraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT) ereport(ERROR, diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 690f4fb620f..6bed1786f9b 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -83,6 +83,7 @@ #define RI_PLAN_NO_ACTION 5 /* For RESTRICT, the same plan can be used for both ON DELETE and ON UPDATE triggers. */ #define RI_PLAN_RESTRICT 6 +/* Temporal SET NULL/DEFAULT reuse these slots too (the period-aware query). */ #define RI_PLAN_SETNULL_ONDELETE 7 #define RI_PLAN_SETNULL_ONUPDATE 8 #define RI_PLAN_SETDEFAULT_ONDELETE 9 @@ -224,6 +225,23 @@ typedef struct RI_CompareHashEntry FmgrInfo cast_func_finfo; /* in case we must coerce input */ } RI_CompareHashEntry; +/* + * Called by tri_foreach_lost_range for each span of history that was lost + * from the referenced table, so that the callback can make the appropriate + * change on the referencing table. + */ +typedef void (*tri_lost_range_callback) (Datum lostRange, void *arg); + +typedef struct TRI_SetContext +{ + const RI_ConstraintInfo *riinfo; + RI_QueryKey *qkey; + SPIPlanPtr qplan; + Relation fk_rel; + Relation pk_rel; + TupleTableSlot *oldslot; +} TRI_SetContext; + /* * Maximum number of FK rows buffered before flushing. * @@ -343,7 +361,8 @@ static void ri_BuildQueryKey(RI_QueryKey *key, const RI_ConstraintInfo *riinfo, int32 constr_queryno); static bool ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, - const RI_ConstraintInfo *riinfo, bool rel_is_pk); + const RI_ConstraintInfo *riinfo, bool rel_is_pk, + bool skip_period); static bool ri_CompareWithCast(Oid eq_opr, Oid typeid, Oid collid, Datum lhs, Datum rhs); @@ -410,7 +429,20 @@ static RI_FastPathEntry *ri_FastPathGetEntry(const RI_ConstraintInfo *riinfo, Relation fk_rel); static void ri_FastPathEndBatch(void *arg); static void ri_FastPathTeardown(int depth); - +static bool fpo_targets_pk_range(const ForPortionOfState *tg_temporal, + const RI_ConstraintInfo *riinfo); +static Datum restrict_enforced_range(const ForPortionOfState *tg_temporal, + const RI_ConstraintInfo *riinfo, + TupleTableSlot *oldslot); +static Datum kept_enforced_range(const ForPortionOfState *tg_temporal, + const RI_ConstraintInfo *riinfo, + TupleTableSlot *oldslot, + TupleTableSlot *newslot); +static void tri_foreach_lost_range(const RI_ConstraintInfo *riinfo, + TupleTableSlot *oldslot, + TupleTableSlot *newslot, + tri_lost_range_callback callback, void *arg); +static void ri_restrict_lost_range(Datum lostRange, void *arg); /* * RI_FKey_check - @@ -669,7 +701,6 @@ RI_FKey_check(TriggerData *trigdata) return PointerGetDatum(NULL); } - /* * RI_FKey_check_ins - * @@ -685,7 +716,6 @@ RI_FKey_check_ins(PG_FUNCTION_ARGS) return RI_FKey_check((TriggerData *) fcinfo->context); } - /* * RI_FKey_check_upd - * @@ -701,7 +731,6 @@ RI_FKey_check_upd(PG_FUNCTION_ARGS) return RI_FKey_check((TriggerData *) fcinfo->context); } - /* * ri_Check_Pk_Match * @@ -833,7 +862,6 @@ ri_Check_Pk_Match(Relation pk_rel, Relation fk_rel, return result; } - /* * RI_FKey_noaction_del - * @@ -921,8 +949,11 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) Relation fk_rel; Relation pk_rel; TupleTableSlot *oldslot; + TupleTableSlot *newslot; RI_QueryKey qkey; SPIPlanPtr qplan; + int targetRangeParam = -1; + Datum targetRange = (Datum) 0; riinfo = ri_FetchConstraintInfo(trigdata->tg_trigger, trigdata->tg_relation, true); @@ -936,6 +967,7 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) fk_rel = table_open(riinfo->fk_relid, RowShareLock); pk_rel = trigdata->tg_relation; oldslot = trigdata->tg_trigslot; + newslot = trigdata->tg_newslot; /* * If another PK row now exists providing the old key values, we should @@ -1027,6 +1059,16 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) * We need the coalesce in case the first subquery returns no rows. * We need the second subquery because FOR KEY SHARE doesn't support * aggregate queries. + * + * For RESTRICT keys we can't query pktable, so instead we use the old + * and new periods to see what was removed, and look for references + * matching that. If the scalar key part changed, then this is + * (where $n is the old period and $2n the new): + * $n && x.fkperiod + * But if the scalar key part didn't change, then we only lost part of + * the time span, so we should look for: + * (SELECT range_agg(r) FROM without_portion($n, $2n) wo(r)) + * && x.fkperiod */ if (riinfo->hasperiod && is_no_action) { @@ -1095,14 +1137,57 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) /* * We have a plan now. Run it to check for existing references. + * + * For RESTRICT constraints with a PERIOD, we must only consider the part + * of history that was lost. + * + * If the scalar key part was UPDATEd without FOR PORTION OF, then all of + * oldslot.pkperiod was lost (whether the endpoints changed or not). + * That's what we already check by default. Otherwise only + * oldslot.pkperiod - newslot.pkperiod was lost. That may be more than + * one range, so we use the without_portion set-returning function and + * loop over its results. It may even be empty, meaning nothing was lost, + * and no check is required. + * + * For a DELETE, all of oldslot.pkperiod was lost, which is what we check + * for by default. */ - ri_PerformCheck(riinfo, &qkey, qplan, - fk_rel, pk_rel, - oldslot, NULL, - -1, (Datum) 0, - !is_no_action, - true, /* must detect new rows */ - SPI_OK_SELECT); + if (riinfo->hasperiod && !is_no_action && + TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event) && + !fpo_targets_pk_range(trigdata->tg_temporal, riinfo) && + ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true, true)) + { + TRI_SetContext ctx; + + ctx.riinfo = riinfo; + ctx.qkey = &qkey; + ctx.qplan = qplan; + ctx.fk_rel = fk_rel; + ctx.pk_rel = pk_rel; + ctx.oldslot = oldslot; + + /* Check the referencing history, one lost span at a time */ + tri_foreach_lost_range(riinfo, oldslot, newslot, + ri_restrict_lost_range, &ctx); + } + else + { + /* Under FOR PORTION OF, don't check more than the targeted portion */ + if (fpo_targets_pk_range(trigdata->tg_temporal, riinfo)) + { + targetRangeParam = riinfo->nkeys; + targetRange = kept_enforced_range(trigdata->tg_temporal, riinfo, + oldslot, newslot); + } + + ri_PerformCheck(riinfo, &qkey, qplan, + fk_rel, pk_rel, + oldslot, NULL, + targetRangeParam, targetRange, + !is_no_action, + true, /* must detect new rows */ + SPI_OK_SELECT); + } if (SPI_finish() != SPI_OK_FINISH) elog(ERROR, "SPI_finish failed"); @@ -1112,7 +1197,6 @@ ri_restrict(TriggerData *trigdata, bool is_no_action) return PointerGetDatum(NULL); } - /* * RI_FKey_cascade_del - * @@ -1215,7 +1299,6 @@ RI_FKey_cascade_del(PG_FUNCTION_ARGS) return PointerGetDatum(NULL); } - /* * RI_FKey_cascade_upd - * @@ -1333,7 +1416,6 @@ RI_FKey_cascade_upd(PG_FUNCTION_ARGS) return PointerGetDatum(NULL); } - /* * RI_FKey_setnull_del - * @@ -1580,7 +1662,6 @@ ri_set(TriggerData *trigdata, bool is_set_null, int tgkind) } } - /* * RI_FKey_pk_upd_check_required - * @@ -1608,7 +1689,7 @@ RI_FKey_pk_upd_check_required(Trigger *trigger, Relation pk_rel, 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)) + if (newslot && ri_KeysEqual(pk_rel, oldslot, newslot, riinfo, true, false)) return false; /* Else we need to fire the trigger. */ @@ -1701,7 +1782,7 @@ RI_FKey_fk_upd_check_required(Trigger *trigger, Relation fk_rel, return true; /* If all old and new key values are equal, no check is needed */ - if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false)) + if (ri_KeysEqual(fk_rel, oldslot, newslot, riinfo, false, false)) return false; /* Else we need to fire the trigger. */ @@ -2225,13 +2306,11 @@ RI_PartitionRemove_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel) AtEOXact_GUC(true, save_nestlevel); } - /* ---------- * Local functions below * ---------- */ - /* * quoteOneName --- safely quote a single SQL name * @@ -2416,7 +2495,6 @@ ri_CheckTrigger(FunctionCallInfo fcinfo, const char *funcname, int tgkind) } } - /* * Fetch the RI_ConstraintInfo struct for the trigger's FK constraint. */ @@ -2678,7 +2756,6 @@ InvalidateConstraintCacheCallBack(Datum arg, SysCacheIdentifier cacheid, } } - /* * Prepare execution plan for a query to enforce an RI restriction */ @@ -3901,7 +3978,6 @@ ri_ReportViolation(const RI_ConstraintInfo *riinfo, errtableconstraint(fk_rel, NameStr(riinfo->conname)))); } - /* * ri_NullCheck - * @@ -3940,7 +4016,6 @@ ri_NullCheck(TupleDesc tupDesc, return RI_KEYS_SOME_NULL; } - /* * ri_InitHashTables - * @@ -3975,7 +4050,6 @@ ri_InitHashTables(void) &ctl, HASH_ELEM | HASH_BLOBS); } - /* * ri_FetchPreparedPlan - * @@ -4028,7 +4102,6 @@ ri_FetchPreparedPlan(RI_QueryKey *key) return NULL; } - /* * ri_HashPreparedPlan - * @@ -4057,14 +4130,14 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan) entry->plan = plan; } - /* * ri_KeysEqual - * * 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 of its old value. + * or the FK side is a subset of its old value. But if skip_period is given, + * then we only compare the non-PERIOD parts of the key. * * 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 @@ -4073,17 +4146,19 @@ ri_HashPreparedPlan(RI_QueryKey *key, SPIPlanPtr plan) */ static bool ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, - const RI_ConstraintInfo *riinfo, bool rel_is_pk) + const RI_ConstraintInfo *riinfo, bool rel_is_pk, bool skip_period) { const int16 *attnums; + Assert(skip_period ? riinfo->hasperiod : true); + if (rel_is_pk) attnums = riinfo->pk_attnums; else attnums = riinfo->fk_attnums; /* XXX: could be worthwhile to fetch all necessary attrs at once */ - for (int i = 0; i < riinfo->nkeys; i++) + for (int i = 0; i < riinfo->nkeys - (skip_period ? 1 : 0); i++) { Datum oldvalue; Datum newvalue; @@ -4146,7 +4221,6 @@ ri_KeysEqual(Relation rel, TupleTableSlot *oldslot, TupleTableSlot *newslot, return true; } - /* * ri_CompareWithCast - * @@ -4301,7 +4375,6 @@ ri_HashCompareOp(Oid eq_opr, Oid typeid) return entry; } - /* * Given a trigger function OID, determine whether it is an RI trigger, * and if so whether it is attached to PK or FK relation. @@ -4708,3 +4781,183 @@ ri_FastPathGetEntry(const RI_ConstraintInfo *riinfo, Relation fk_rel) return entry; } + +/* + * fpo_targets_pk_range + * + * Returns true iff the primary key referenced by riinfo includes the range + * column targeted by the FOR PORTION OF clause (according to tg_temporal). + */ +static bool +fpo_targets_pk_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo) +{ + if (tg_temporal == NULL) + return false; + + return riinfo->pk_attnums[riinfo->nkeys - 1] == tg_temporal->fp_rangeAttno; +} + +/* + * restrict_enforced_range - + * + * Returns a Datum of RangeTypeP (or MultirangeTypeP) holding the appropriate + * timespan to target child records when we CASCADE/SET NULL/SET DEFAULT. + * + * In a normal UPDATE/DELETE this should be the referenced row's own valid time, + * but if there was a FOR PORTION OF clause, then we should use that to + * trim down the span further. + */ +static Datum +restrict_enforced_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, TupleTableSlot *oldslot) +{ + Datum pkRecordRange; + bool isnull; + AttrNumber attno = riinfo->pk_attnums[riinfo->nkeys - 1]; + + pkRecordRange = slot_getattr(oldslot, attno, &isnull); + if (isnull) + elog(ERROR, "application time should not be null"); + + if (fpo_targets_pk_range(tg_temporal, riinfo)) + { + if (!OidIsValid(riinfo->period_intersect_proc)) + elog(ERROR, "invalid intersect support function"); + + return OidFunctionCall2(riinfo->period_intersect_proc, pkRecordRange, tg_temporal->fp_targetRange); + } + else + return pkRecordRange; +} + +/* + * kept_enforced_range - + * + * Like restrict_enforced_range, but for an UPDATE of the referenced row, which + * may shorten the row's valid time as well as change its key. + * + * Returns the history the row kept, which is the only history where a + * referencing row can still follow the row's new key. + */ +static Datum +kept_enforced_range(const ForPortionOfState *tg_temporal, const RI_ConstraintInfo *riinfo, + TupleTableSlot *oldslot, TupleTableSlot *newslot) +{ + Datum oldRange; + Datum newRange; + bool isnull; + AttrNumber attno = riinfo->pk_attnums[riinfo->nkeys - 1]; + + /* + * With FOR PORTION OF the row keeps the history outside the targeted + * bounds as temporal leftovers, so the targeted history is the only + * history it kept *and* changed. + */ + if (fpo_targets_pk_range(tg_temporal, riinfo)) + return restrict_enforced_range(tg_temporal, riinfo, oldslot); + + if (!OidIsValid(riinfo->period_intersect_proc)) + elog(ERROR, "invalid intersect support function"); + + oldRange = slot_getattr(oldslot, attno, &isnull); + if (isnull) + elog(ERROR, "application time should not be null"); + newRange = slot_getattr(newslot, attno, &isnull); + if (isnull) + elog(ERROR, "application time should not be null"); + + return OidFunctionCall2(riinfo->period_intersect_proc, oldRange, newRange); +} + +/* + * tri_foreach_lost_range - + * + * Computes the history a referenced row lost when it was updated, and invokes + * the callback once for each lost span. + * + * This uses the without_portion SRF helper, just as FOR PORTION OF does to + * compute temporal leftovers. We expect ranges to yield 0-2 lost sections; + * multiranges, 0-1; and (XXX: someday) user-defined types, any number. + * But if the original command used FOR PORTION OF, then the lost range is + * simply the targeted history intersected with the original period, because + * temporal leftovers preserve the untargeted portion. + */ +static void +tri_foreach_lost_range(const RI_ConstraintInfo *riinfo, + TupleTableSlot *oldslot, TupleTableSlot *newslot, + tri_lost_range_callback callback, void *arg) +{ + FmgrInfo flinfo; + ReturnSetInfo rsi; + AttrNumber attno = riinfo->pk_attnums[riinfo->nkeys - 1]; + Datum oldRange; + Datum newRange; + bool isnull; + + LOCAL_FCINFO(fcinfo, 2); + + oldRange = slot_getattr(oldslot, attno, &isnull); + if (isnull) + elog(ERROR, "application time should not be null"); + newRange = slot_getattr(newslot, attno, &isnull); + if (isnull) + elog(ERROR, "application time should not be null"); + + fmgr_info(riinfo->period_without_portion_proc, &flinfo); + rsi.type = T_ReturnSetInfo; + rsi.econtext = CreateStandaloneExprContext(); + rsi.expectedDesc = NULL; + rsi.allowedModes = (int) (SFRM_ValuePerCall); + rsi.returnMode = SFRM_ValuePerCall; + /* isDone is filled below */ + rsi.setResult = NULL; + rsi.setDesc = NULL; + + InitFunctionCallInfoData(*fcinfo, &flinfo, 2, InvalidOid, NULL, (Node *) &rsi); + fcinfo->args[0].value = oldRange; + fcinfo->args[0].isnull = false; + fcinfo->args[1].value = newRange; + fcinfo->args[1].isnull = false; + + for (;;) + { + Datum lost; + + fcinfo->isnull = false; + rsi.isDone = ExprSingleResult; + lost = FunctionCallInvoke(fcinfo); + + if (rsi.returnMode != SFRM_ValuePerCall) + elog(ERROR, "without_portion function violated function call protocol"); + + /* Are we done? */ + if (rsi.isDone == ExprEndResult) + break; + + if (fcinfo->isnull) + elog(ERROR, "got a null from without_portion function"); + + callback(lost, arg); + } + + FreeExprContext(rsi.econtext, true); +} + +/* + * ri_restrict_lost_range - + * + * tri_foreach_lost_range callback for ON UPDATE RESTRICT: runs the prepared + * check against one span the referenced row lost. + */ +static void +ri_restrict_lost_range(Datum lostRange, void *arg) +{ + TRI_SetContext *ctx = (TRI_SetContext *) arg; + + ri_PerformCheck(ctx->riinfo, ctx->qkey, ctx->qplan, + ctx->fk_rel, ctx->pk_rel, + ctx->oldslot, NULL, + ctx->riinfo->nkeys, lostRange, + true, + true, /* must detect new rows */ + SPI_OK_SELECT); +} diff --git a/src/test/regress/expected/without_overlaps.out b/src/test/regress/expected/without_overlaps.out index de2f8bc4786..5d947bd88de 100644 --- a/src/test/regress/expected/without_overlaps.out +++ b/src/test/regress/expected/without_overlaps.out @@ -1903,11 +1903,119 @@ ALTER TABLE temporal_fk_rng2rng FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng ON UPDATE RESTRICT; -ERROR: unsupported ON UPDATE action for foreign key constraint using PERIOD +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01')); +UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') WHERE id = '[5,6)'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM temporal_rng WHERE id = '[5,6)'; +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[5,6)', daterange('2018-01-01', '2018-02-01')), + ('[5,6)', daterange('2018-02-01', '2018-03-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) + VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)'); +UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01') + WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01'); +-- A PK update sliding the edge between two referenced rows +-- (fails, unlike NO ACTION): +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[6,7)', daterange('2018-01-01', '2018-02-01')), + ('[6,7)', daterange('2018-02-01', '2018-03-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES + ('[4,5)', daterange('2018-01-15', '2018-02-15'), '[6,7)'); +UPDATE temporal_rng + SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05') + WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END + WHERE id = '[6,7)'; +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([6,7), [2018-01-01,2018-02-01)) is referenced from table "temporal_fk_rng2rng". +-- a PK update shrinking the referenced range but still valid: +-- There are two references: one fulfilled by the first pk row, +-- the other fulfilled by both pk rows combined. +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[1,2)', daterange('2018-01-01', '2018-03-01')), + ('[1,2)', daterange('2018-03-01', '2018-06-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES + ('[1,2)', daterange('2018-01-15', '2018-02-01'), '[1,2)'), + ('[2,3)', daterange('2018-01-15', '2018-05-01'), '[1,2)'); +UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-03-01') + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +-- a PK update growing the referenced range is fine: +UPDATE temporal_rng SET valid_at = daterange('2018-01-01', '2018-03-01') + WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date; +-- a PK update shrinking the referenced range and changing the id invalidates the whole range (error): +UPDATE temporal_rng SET id = '[2,3)', valid_at = daterange('2018-01-15', '2018-03-01') + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([1,2), [2018-01-01,2018-03-01)) is referenced from table "temporal_fk_rng2rng". +-- a PK update changing only the id invalidates the whole range (error): +UPDATE temporal_rng SET id = '[2,3)' + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([1,2), [2018-01-01,2018-03-01)) is referenced from table "temporal_fk_rng2rng". +-- a PK update that loses time from both ends, but is still valid: +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[2,3)', daterange('2018-01-01', '2018-03-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES + ('[5,6)', daterange('2018-01-15', '2018-02-01'), '[2,3)'); +UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-02-15') + WHERE id = '[2,3)'; +-- a PK update that fails because both are referenced: +UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') + WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is referenced from table "temporal_fk_rng2rng". +-- a PK update that fails because both are referenced, even before commit: +BEGIN; + ALTER TABLE temporal_fk_rng2rng + ALTER CONSTRAINT temporal_fk_rng2rng_fk + DEFERRABLE INITIALLY DEFERRED; + UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') + WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is referenced from table "temporal_fk_rng2rng". +ROLLBACK; +-- changing the scalar part fails: +UPDATE temporal_rng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is referenced from table "temporal_fk_rng2rng". +-- changing an unreferenced part is okay: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' + SET id = '[7,8)' + WHERE id = '[5,6)'; +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is referenced from table "temporal_fk_rng2rng". +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; + id | valid_at +-------+------------------------- + [5,6) | [2016-02-01,2016-03-01) + [5,6) | [2018-01-01,2018-01-02) + [5,6) | [2018-01-03,2018-02-01) + [7,8) | [2018-01-02,2018-01-03) +(4 rows) + +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; + id | valid_at | parent_id +-------+-------------------------+----------- + [3,4) | [2018-01-05,2018-01-10) | [5,6) +(1 row) + +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; +UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') + WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); -- -- test FK referenced deletes NO ACTION -- TRUNCATE temporal_rng, temporal_fk_rng2rng; +ALTER TABLE temporal_fk_rng2rng + DROP CONSTRAINT temporal_fk_rng2rng_fk; ALTER TABLE temporal_fk_rng2rng ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1985,7 +2093,68 @@ ALTER TABLE temporal_fk_rng2rng FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng ON DELETE RESTRICT; -ERROR: unsupported ON DELETE action for foreign key constraint using PERIOD +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01')); +DELETE FROM temporal_rng WHERE id = '[5,6)'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[5,6)', daterange('2018-01-01', '2018-02-01')), + ('[5,6)', daterange('2018-02-01', '2018-03-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES + ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)'); +DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is referenced from table "temporal_fk_rng2rng". +-- a PK delete that fails because both are referenced, even before commit: +BEGIN; + ALTER TABLE temporal_fk_rng2rng + ALTER CONSTRAINT temporal_fk_rng2rng_fk + DEFERRABLE INITIALLY DEFERRED; + DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-01,2018-02-01)) is referenced from table "temporal_fk_rng2rng". +ROLLBACK; +-- deleting an unreferenced part is okay: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' +WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; + id | valid_at +-------+------------------------- + [5,6) | [2018-01-01,2018-01-02) + [5,6) | [2018-01-03,2018-02-01) +(2 rows) + +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; + id | valid_at | parent_id +-------+-------------------------+----------- + [3,4) | [2018-01-05,2018-01-10) | [5,6) +(1 row) + +-- deleting just a part fails: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' +WHERE id = '[5,6)'; +ERROR: update or delete on table "temporal_rng" violates RESTRICT setting of foreign key constraint "temporal_fk_rng2rng_fk" on table "temporal_fk_rng2rng" +DETAIL: Key (id, valid_at)=([5,6), [2018-01-03,2018-02-01)) is referenced from table "temporal_fk_rng2rng". +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; + id | valid_at +-------+------------------------- + [5,6) | [2018-01-01,2018-01-02) + [5,6) | [2018-01-03,2018-02-01) +(2 rows) + +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; + id | valid_at | parent_id +-------+-------------------------+----------- + [3,4) | [2018-01-05,2018-01-10) | [5,6) +(1 row) + +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; +DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); -- -- rng2rng test ON UPDATE/DELETE options -- @@ -1994,6 +2163,7 @@ TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng + DROP CONSTRAINT temporal_fk_rng2rng_fk, ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng @@ -2008,7 +2178,7 @@ ALTER TABLE temporal_fk_rng2rng FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng ON DELETE SET NULL ON UPDATE SET NULL; -ERROR: unsupported ON UPDATE action for foreign key constraint using PERIOD +ERROR: constraint "temporal_fk_rng2rng_fk" for relation "temporal_fk_rng2rng" already exists -- test FK referenced updates SET DEFAULT TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[-1,-1]', daterange(null, null)); @@ -2020,7 +2190,7 @@ ALTER TABLE temporal_fk_rng2rng FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng ON DELETE SET DEFAULT ON UPDATE SET DEFAULT; -ERROR: unsupported ON UPDATE action for foreign key constraint using PERIOD +ERROR: constraint "temporal_fk_rng2rng_fk" for relation "temporal_fk_rng2rng" already exists -- -- test FOREIGN KEY, multirange references multirange -- @@ -2410,11 +2580,118 @@ ALTER TABLE temporal_fk_mltrng2mltrng FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_mltrng (id, PERIOD valid_at) ON UPDATE RESTRICT; -ERROR: unsupported ON UPDATE action for foreign key constraint using PERIOD +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))); +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) WHERE id = '[5,6)'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM temporal_mltrng WHERE id = '[5,6)'; +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))), + ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES + ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)'); +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01')) + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01')); +-- A PK update sliding the edge between two referenced rows +-- (fails, unlike NO ACTION): +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[6,7)', datemultirange(daterange('2018-01-01', '2018-02-01'))), + ('[6,7)', datemultirange(daterange('2018-02-01', '2018-03-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES + ('[4,5)', datemultirange(daterange('2018-01-15', '2018-02-15')), '[6,7)'); +UPDATE temporal_mltrng + SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05')) + WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END + WHERE id = '[6,7)'; +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([6,7), {[2018-01-01,2018-02-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +-- a PK update shrinking the referenced multirange but still valid: +-- There are two references: one fulfilled by the first pk row, +-- the other fulfilled by both pk rows combined. +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[1,2)', datemultirange(daterange('2018-01-01', '2018-03-01'))), + ('[1,2)', datemultirange(daterange('2018-03-01', '2018-06-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES + ('[1,2)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[1,2)'), + ('[2,3)', datemultirange(daterange('2018-01-15', '2018-05-01')), '[1,2)'); +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-03-01')) + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +-- a PK update growing the referenced multirange is fine: +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-03-01')) + WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date; +-- a PK update shrinking the referenced multirange and changing the id invalidates the whole multirange (error): +UPDATE temporal_mltrng SET id = '[2,3)', valid_at = datemultirange(daterange('2018-01-15', '2018-03-01')) + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([1,2), {[2018-01-01,2018-03-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +-- a PK update changing only the id invalidates the whole multirange (error): +UPDATE temporal_mltrng SET id = '[2,3)' + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([1,2), {[2018-01-01,2018-03-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +-- a PK update that loses time from both ends, but is still valid: +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[2,3)', datemultirange(daterange('2018-01-01', '2018-03-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES + ('[5,6)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[2,3)'); +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-02-15')) + WHERE id = '[2,3)'; +-- a PK update that fails because both are referenced: +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +-- a PK update that fails because both are referenced, even before commit: +BEGIN; + ALTER TABLE temporal_fk_mltrng2mltrng + ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk + DEFERRABLE INITIALLY DEFERRED; + UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +ROLLBACK; +-- changing the scalar part fails: +UPDATE temporal_mltrng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +-- changing an unreferenced part is okay: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +SELECT * FROM temporal_mltrng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; + id | valid_at +-------+--------------------------------------------------- + [5,6) | {[2016-02-01,2016-03-01)} + [5,6) | {[2018-01-01,2018-01-02),[2018-01-03,2018-02-01)} + [7,8) | {[2018-01-02,2018-01-03)} +(3 rows) + +SELECT * FROM temporal_fk_mltrng2mltrng WHERE id in ('[3,4)') ORDER BY id, valid_at; + id | valid_at | parent_id +-------+---------------------------+----------- + [3,4) | {[2018-01-05,2018-01-10)} | [5,6) +(1 row) + +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; +UPDATE temporal_mltrng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); -- -- test FK referenced deletes NO ACTION -- TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng; +ALTER TABLE temporal_fk_mltrng2mltrng + DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk; ALTER TABLE temporal_fk_mltrng2mltrng ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -2455,6 +2732,52 @@ DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02 DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); -- +-- test FK referenced deletes RESTRICT +-- +TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng; +ALTER TABLE temporal_fk_mltrng2mltrng + DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk; +ALTER TABLE temporal_fk_mltrng2mltrng + ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES temporal_mltrng (id, PERIOD valid_at) + ON DELETE RESTRICT; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))); +DELETE FROM temporal_mltrng WHERE id = '[5,6)'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))), + ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)'); +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01')); +-- a PK delete that fails because both are referenced: +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +-- a PK delete that fails because both are referenced, even before commit: +BEGIN; + ALTER TABLE temporal_fk_mltrng2mltrng + ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk + DEFERRABLE INITIALLY DEFERRED; + DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-02-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +ROLLBACK; +-- deleting an unreferenced part is okay: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) +WHERE id = '[5,6)'; +-- deleting just a part fails: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) +WHERE id = '[5,6)'; +ERROR: update or delete on table "temporal_mltrng" violates RESTRICT setting of foreign key constraint "temporal_fk_mltrng2mltrng_fk" on table "temporal_fk_mltrng2mltrng" +DETAIL: Key (id, valid_at)=([5,6), {[2018-01-01,2018-01-02),[2018-01-03,2018-02-01)}) is referenced from table "temporal_fk_mltrng2mltrng". +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +-- -- FK between partitioned tables: ranges -- CREATE TABLE temporal_partitioned_rng ( diff --git a/src/test/regress/sql/without_overlaps.sql b/src/test/regress/sql/without_overlaps.sql index 4833b8ac5f0..880e7d99467 100644 --- a/src/test/regress/sql/without_overlaps.sql +++ b/src/test/regress/sql/without_overlaps.sql @@ -1393,12 +1393,95 @@ ALTER TABLE temporal_fk_rng2rng FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng ON UPDATE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01')); +UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') WHERE id = '[5,6)'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM temporal_rng WHERE id = '[5,6)'; +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[5,6)', daterange('2018-01-01', '2018-02-01')), + ('[5,6)', daterange('2018-02-01', '2018-03-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) + VALUES ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)'); +UPDATE temporal_rng SET valid_at = daterange('2016-02-01', '2016-03-01') + WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01'); +-- A PK update sliding the edge between two referenced rows +-- (fails, unlike NO ACTION): +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[6,7)', daterange('2018-01-01', '2018-02-01')), + ('[6,7)', daterange('2018-02-01', '2018-03-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES + ('[4,5)', daterange('2018-01-15', '2018-02-15'), '[6,7)'); +UPDATE temporal_rng + SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN daterange('2018-01-01', '2018-01-05') + WHEN lower(valid_at) = '2018-02-01' THEN daterange('2018-01-05', '2018-03-01') END + WHERE id = '[6,7)'; +-- a PK update shrinking the referenced range but still valid: +-- There are two references: one fulfilled by the first pk row, +-- the other fulfilled by both pk rows combined. +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[1,2)', daterange('2018-01-01', '2018-03-01')), + ('[1,2)', daterange('2018-03-01', '2018-06-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES + ('[1,2)', daterange('2018-01-15', '2018-02-01'), '[1,2)'), + ('[2,3)', daterange('2018-01-15', '2018-05-01'), '[1,2)'); +UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-03-01') + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +-- a PK update growing the referenced range is fine: +UPDATE temporal_rng SET valid_at = daterange('2018-01-01', '2018-03-01') + WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date; +-- a PK update shrinking the referenced range and changing the id invalidates the whole range (error): +UPDATE temporal_rng SET id = '[2,3)', valid_at = daterange('2018-01-15', '2018-03-01') + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +-- a PK update changing only the id invalidates the whole range (error): +UPDATE temporal_rng SET id = '[2,3)' + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +-- a PK update that loses time from both ends, but is still valid: +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[2,3)', daterange('2018-01-01', '2018-03-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES + ('[5,6)', daterange('2018-01-15', '2018-02-01'), '[2,3)'); +UPDATE temporal_rng SET valid_at = daterange('2018-01-15', '2018-02-15') + WHERE id = '[2,3)'; +-- a PK update that fails because both are referenced: +UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') + WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +-- a PK update that fails because both are referenced, even before commit: +BEGIN; + ALTER TABLE temporal_fk_rng2rng + ALTER CONSTRAINT temporal_fk_rng2rng_fk + DEFERRABLE INITIALLY DEFERRED; + + UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') + WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +ROLLBACK; +-- changing the scalar part fails: +UPDATE temporal_rng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +-- changing an unreferenced part is okay: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_rng + FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' + SET id = '[7,8)' + WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; +UPDATE temporal_rng SET valid_at = daterange('2016-01-01', '2016-02-01') + WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); -- -- test FK referenced deletes NO ACTION -- TRUNCATE temporal_rng, temporal_fk_rng2rng; +ALTER TABLE temporal_fk_rng2rng + DROP CONSTRAINT temporal_fk_rng2rng_fk; ALTER TABLE temporal_fk_rng2rng ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1451,6 +1534,41 @@ ALTER TABLE temporal_fk_rng2rng FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng ON DELETE RESTRICT; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO temporal_rng (id, valid_at) VALUES ('[5,6)', daterange('2018-01-01', '2018-02-01')); +DELETE FROM temporal_rng WHERE id = '[5,6)'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO temporal_rng (id, valid_at) VALUES + ('[5,6)', daterange('2018-01-01', '2018-02-01')), + ('[5,6)', daterange('2018-02-01', '2018-03-01')); +INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES + ('[3,4)', daterange('2018-01-05', '2018-01-10'), '[5,6)'); +DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-02-01', '2018-03-01'); +-- a PK delete that fails because both are referenced: +DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +-- a PK delete that fails because both are referenced, even before commit: +BEGIN; + ALTER TABLE temporal_fk_rng2rng + ALTER CONSTRAINT temporal_fk_rng2rng_fk + DEFERRABLE INITIALLY DEFERRED; + + DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); +ROLLBACK; +-- deleting an unreferenced part is okay: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-02' TO '2018-01-03' +WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; +-- deleting just a part fails: +DELETE FROM temporal_rng +FOR PORTION OF valid_at FROM '2018-01-05' TO '2018-01-10' +WHERE id = '[5,6)'; +SELECT * FROM temporal_rng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_rng2rng WHERE id in ('[3,4)') ORDER BY id, valid_at; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM temporal_fk_rng2rng WHERE id = '[3,4)'; +DELETE FROM temporal_rng WHERE id = '[5,6)' AND valid_at = daterange('2018-01-01', '2018-02-01'); -- -- rng2rng test ON UPDATE/DELETE options @@ -1461,6 +1579,7 @@ TRUNCATE temporal_rng, temporal_fk_rng2rng; INSERT INTO temporal_rng (id, valid_at) VALUES ('[6,7)', daterange('2018-01-01', '2021-01-01')); INSERT INTO temporal_fk_rng2rng (id, valid_at, parent_id) VALUES ('[100,101)', daterange('2018-01-01', '2021-01-01'), '[6,7)'); ALTER TABLE temporal_fk_rng2rng + DROP CONSTRAINT temporal_fk_rng2rng_fk, ADD CONSTRAINT temporal_fk_rng2rng_fk FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_rng @@ -1845,12 +1964,95 @@ ALTER TABLE temporal_fk_mltrng2mltrng FOREIGN KEY (parent_id, PERIOD valid_at) REFERENCES temporal_mltrng (id, PERIOD valid_at) ON UPDATE RESTRICT; +-- a PK update that succeeds because the numeric id isn't referenced: +INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))); +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) WHERE id = '[5,6)'; +-- a PK update that succeeds even though the numeric id is referenced because the range isn't: +DELETE FROM temporal_mltrng WHERE id = '[5,6)'; +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))), + ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES + ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)'); +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-02-01', '2016-03-01')) + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01')); +-- A PK update sliding the edge between two referenced rows +-- (fails, unlike NO ACTION): +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[6,7)', datemultirange(daterange('2018-01-01', '2018-02-01'))), + ('[6,7)', datemultirange(daterange('2018-02-01', '2018-03-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES + ('[4,5)', datemultirange(daterange('2018-01-15', '2018-02-15')), '[6,7)'); +UPDATE temporal_mltrng + SET valid_at = CASE WHEN lower(valid_at) = '2018-01-01' THEN datemultirange(daterange('2018-01-01', '2018-01-05')) + WHEN lower(valid_at) = '2018-02-01' THEN datemultirange(daterange('2018-01-05', '2018-03-01')) END + WHERE id = '[6,7)'; +-- a PK update shrinking the referenced multirange but still valid: +-- There are two references: one fulfilled by the first pk row, +-- the other fulfilled by both pk rows combined. +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[1,2)', datemultirange(daterange('2018-01-01', '2018-03-01'))), + ('[1,2)', datemultirange(daterange('2018-03-01', '2018-06-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES + ('[1,2)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[1,2)'), + ('[2,3)', datemultirange(daterange('2018-01-15', '2018-05-01')), '[1,2)'); +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-03-01')) + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +-- a PK update growing the referenced multirange is fine: +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-01', '2018-03-01')) + WHERE id = '[1,2)' AND valid_at @> '2018-01-25'::date; +-- a PK update shrinking the referenced multirange and changing the id invalidates the whole multirange (error): +UPDATE temporal_mltrng SET id = '[2,3)', valid_at = datemultirange(daterange('2018-01-15', '2018-03-01')) + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +-- a PK update changing only the id invalidates the whole multirange (error): +UPDATE temporal_mltrng SET id = '[2,3)' + WHERE id = '[1,2)' AND valid_at @> '2018-01-15'::date; +-- a PK update that loses time from both ends, but is still valid: +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[2,3)', datemultirange(daterange('2018-01-01', '2018-03-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES + ('[5,6)', datemultirange(daterange('2018-01-15', '2018-02-01')), '[2,3)'); +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2018-01-15', '2018-02-15')) + WHERE id = '[2,3)'; +-- a PK update that fails because both are referenced: +UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +-- a PK update that fails because both are referenced, even before commit: +BEGIN; + ALTER TABLE temporal_fk_mltrng2mltrng + ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk + DEFERRABLE INITIALLY DEFERRED; + + UPDATE temporal_mltrng SET valid_at = datemultirange(daterange('2016-01-01', '2016-02-01')) + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +ROLLBACK; +-- changing the scalar part fails: +UPDATE temporal_mltrng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +-- changing an unreferenced part is okay: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +-- changing just a part fails: +UPDATE temporal_mltrng + FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) + SET id = '[7,8)' + WHERE id = '[5,6)'; +SELECT * FROM temporal_mltrng WHERE id in ('[5,6)', '[7,8)') ORDER BY id, valid_at; +SELECT * FROM temporal_fk_mltrng2mltrng WHERE id in ('[3,4)') ORDER BY id, valid_at; +-- then delete the objecting FK record and the same PK update succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; +UPDATE temporal_mltrng SET id = '[7,8)' + WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); -- -- test FK referenced deletes NO ACTION -- TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng; +ALTER TABLE temporal_fk_mltrng2mltrng + DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk; ALTER TABLE temporal_fk_mltrng2mltrng ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk FOREIGN KEY (parent_id, PERIOD valid_at) @@ -1886,6 +2088,49 @@ WHERE id = '[5,6)'; DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +-- +-- test FK referenced deletes RESTRICT +-- + +TRUNCATE temporal_mltrng, temporal_fk_mltrng2mltrng; +ALTER TABLE temporal_fk_mltrng2mltrng + DROP CONSTRAINT temporal_fk_mltrng2mltrng_fk; +ALTER TABLE temporal_fk_mltrng2mltrng + ADD CONSTRAINT temporal_fk_mltrng2mltrng_fk + FOREIGN KEY (parent_id, PERIOD valid_at) + REFERENCES temporal_mltrng (id, PERIOD valid_at) + ON DELETE RESTRICT; +-- a PK delete that succeeds because the numeric id isn't referenced: +INSERT INTO temporal_mltrng (id, valid_at) VALUES ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))); +DELETE FROM temporal_mltrng WHERE id = '[5,6)'; +-- a PK delete that succeeds even though the numeric id is referenced because the range isn't: +INSERT INTO temporal_mltrng (id, valid_at) VALUES + ('[5,6)', datemultirange(daterange('2018-01-01', '2018-02-01'))), + ('[5,6)', datemultirange(daterange('2018-02-01', '2018-03-01'))); +INSERT INTO temporal_fk_mltrng2mltrng (id, valid_at, parent_id) VALUES ('[3,4)', datemultirange(daterange('2018-01-05', '2018-01-10')), '[5,6)'); +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-02-01', '2018-03-01')); +-- a PK delete that fails because both are referenced: +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +-- a PK delete that fails because both are referenced, even before commit: +BEGIN; + ALTER TABLE temporal_fk_mltrng2mltrng + ALTER CONSTRAINT temporal_fk_mltrng2mltrng_fk + DEFERRABLE INITIALLY DEFERRED; + + DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); +ROLLBACK; +-- deleting an unreferenced part is okay: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-02', '2018-01-03'))) +WHERE id = '[5,6)'; +-- deleting just a part fails: +DELETE FROM temporal_mltrng +FOR PORTION OF valid_at (datemultirange(daterange('2018-01-05', '2018-01-10'))) +WHERE id = '[5,6)'; +-- then delete the objecting FK record and the same PK delete succeeds: +DELETE FROM temporal_fk_mltrng2mltrng WHERE id = '[3,4)'; +DELETE FROM temporal_mltrng WHERE id = '[5,6)' AND valid_at = datemultirange(daterange('2018-01-01', '2018-02-01')); + -- -- FK between partitioned tables: ranges -- diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index c546b3d6375..7214cb29a81 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3118,6 +3118,7 @@ TParserStateAction TParserStateActionItem TQueueDestReceiver TRGM +TRI_SetContext TSAnyCacheEntry TSConfigCacheEntry TSConfigInfo -- 2.45.0