diff --git a/coccinelle/namedata.cocci b/coccinelle/namedata.cocci index f3188ae525e..e1469be14c0 100644 --- a/coccinelle/namedata.cocci +++ b/coccinelle/namedata.cocci @@ -16,12 +16,13 @@ struct I1 } @rule_namedata_strlcpy@ -expression E1, E2; +identifier I1; +expression E1; symbol NAMEDATALEN; @@ -- strlcpy(E1, E2, NAMEDATALEN); +- strlcpy(I1, E1, NAMEDATALEN); + /* You are using strlcpy with NAMEDATALEN, please consider using NameData and namestrcpy instead. */ -+ namestrcpy(E1, E2); ++ namestrcpy(I1, E1); @rule_namedata_memcpy@ expression E1, E2; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 650780bab42..8c3d72e3e80 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ set(SOURCES extension.c extension_constants.c expression_utils.c + foreign_key.c gapfill.c guc.c histogram.c diff --git a/src/chunk_constraint.c b/src/chunk_constraint.c index d6d9219b086..87bbeb60058 100644 --- a/src/chunk_constraint.c +++ b/src/chunk_constraint.c @@ -33,6 +33,7 @@ #include "dimension_vector.h" #include "errors.h" #include "export.h" +#include "foreign_key.h" #include "hypercube.h" #include "hypertable.h" #include "partitioning.h" @@ -512,6 +513,9 @@ ts_chunk_constraints_create(const Hypertable *ht, const Chunk *chunk) Assert(list_length(cookedconstrs) == list_length(newconstrs)); CommandCounterIncrement(); } + + /* Copy FK triggers to this chunk */ + ts_chunk_copy_referencing_fk(ht, chunk); } ScanIterator diff --git a/src/foreign_key.c b/src/foreign_key.c new file mode 100644 index 00000000000..f7c4b6f862e --- /dev/null +++ b/src/foreign_key.c @@ -0,0 +1,574 @@ +/* + * This file and its contents are licensed under the Apache License 2.0. + * Please see the included NOTICE for copyright information and + * LICENSE-APACHE for a copy of the license. + */ + +/* + * The table referenced by a foreign constraint is supposed to have a + * constraint that prevents removing the referenced rows. The constraint + * is enforced by a pair of update and delete triggers. Normally this + * is done by the postgres addFkRecurseReferenced(), but it doesn't work + * for hypertables because they use inheritance, and that function only + * recurses into declarative partitioning hierarchy. + */ + +#include +#include "access/attmap.h" +#include "catalog/pg_trigger.h" +#include "commands/trigger.h" +#include "parser/parser.h" + +#include "compat/compat.h" +#include "chunk.h" +#include "foreign_key.h" +#include "hypertable.h" + +static HeapTuple relation_get_fk_constraint(Oid conrelid, Oid confrelid); +static List *relation_get_referencing_fk(Oid reloid); +static Oid get_fk_index(Relation rel, int nkeys, AttrNumber *confkeys); +static void constraint_get_trigger(Oid conoid, Oid *updtrigoid, Oid *deltrigoid); +static char *ChooseForeignKeyConstraintNameAddition(int numkeys, AttrNumber *keys, Oid relid); +static void createForeignKeyActionTriggers(Form_pg_constraint fk, Oid relid, Oid refRelOid, + Oid constraintOid, Oid indexOid, Oid parentDelTrigger, + Oid parentUpdTrigger); +static void clone_constraint_on_chunk(const Chunk *chunk, Relation parentRel, Form_pg_constraint fk, + int numfks, AttrNumber *conkey, AttrNumber *confkey, + Oid *conpfeqop, Oid *conppeqop, Oid *conffeqop, +#if PG15_GE + int numfkdelsetcols, AttrNumber *confdelsetcols, +#endif + Oid parentDelTrigger, Oid parentUpdTrigger); + +/* + * Copy foreign key constraint fk_tuple to all chunks. + */ +static void +propagate_fk(Relation ht_rel, HeapTuple fk_tuple, List *chunks) +{ + Form_pg_constraint fk = (Form_pg_constraint) GETSTRUCT(fk_tuple); + + int numfks; + AttrNumber conkey[INDEX_MAX_KEYS]; + AttrNumber confkey[INDEX_MAX_KEYS]; + Oid conpfeqop[INDEX_MAX_KEYS]; + Oid conppeqop[INDEX_MAX_KEYS]; + Oid conffeqop[INDEX_MAX_KEYS]; +#if PG15_GE + int numfkdelsetcols; + AttrNumber confdelsetcols[INDEX_MAX_KEYS]; +#endif + + DeconstructFkConstraintRow(fk_tuple, + &numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop +#if PG15_GE + , + &numfkdelsetcols, + confdelsetcols +#endif + ); + + Oid parentDelTrigger, parentUpdTrigger; + constraint_get_trigger(fk->oid, &parentUpdTrigger, &parentDelTrigger); + + ListCell *lc; + foreach (lc, chunks) + { + Chunk *chunk = lfirst(lc); + clone_constraint_on_chunk(chunk, + ht_rel, + fk, + numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop, +#if PG15_GE + numfkdelsetcols, + confdelsetcols, +#endif + parentDelTrigger, + parentUpdTrigger); + } +} + +/* + * Copy all foreign key constraints from the main table to a chunk. + */ +void +ts_chunk_copy_referencing_fk(const Hypertable *ht, const Chunk *chunk) +{ + ListCell *lc; + List *chunks = list_make1((Chunk *) chunk); + List *fks = relation_get_referencing_fk(ht->main_table_relid); + + Relation ht_rel = table_open(ht->main_table_relid, AccessShareLock); + foreach (lc, fks) + { + HeapTuple fk_tuple = lfirst(lc); + propagate_fk(ht_rel, fk_tuple, chunks); + } + table_close(ht_rel, NoLock); +} + +/* + * Copy one foreign key constraint from the main table to all chunks. + */ +void +ts_fk_propagate(Oid conrelid, Hypertable *ht) +{ + HeapTuple fk_tuple = relation_get_fk_constraint(conrelid, ht->main_table_relid); + + if (!fk_tuple) + elog(ERROR, "foreign key constraint not found"); + + Relation ht_rel = table_open(ht->main_table_relid, AccessShareLock); + List *chunks = ts_chunk_get_by_hypertable_id(ht->fd.id); + propagate_fk(ht_rel, fk_tuple, chunks); + table_close(ht_rel, NoLock); +} + +/* + * Clone a single constraint to a single chunk. + */ +static void +clone_constraint_on_chunk(const Chunk *chunk, Relation parentRel, Form_pg_constraint fk, int numfks, + AttrNumber *conkey, AttrNumber *confkey, Oid *conpfeqop, Oid *conppeqop, + Oid *conffeqop, +#if PG15_GE + int numfkdelsetcols, AttrNumber *confdelsetcols, +#endif + Oid parentDelTrigger, Oid parentUpdTrigger) +{ + AttrNumber mapped_confkey[INDEX_MAX_KEYS]; + Relation pkrel = table_open(chunk->table_id, AccessShareLock); + + /* Map the foreign key columns on the hypertable side to the chunk columns */ +#if PG16_GE + AttrMap *attmap = + build_attrmap_by_name(RelationGetDescr(pkrel), RelationGetDescr(parentRel), false); +#else + AttrMap *attmap = build_attrmap_by_name(RelationGetDescr(pkrel), RelationGetDescr(parentRel)); +#endif + for (int i = 0; i < numfks; i++) + mapped_confkey[i] = attmap->attnums[confkey[i] - 1]; + + Oid indexoid = get_fk_index(pkrel, numfks, mapped_confkey); + /* Since postgres accepted the constraint, there should be a supporting index. */ + Ensure(OidIsValid(indexoid), "index for constraint not found on chunk"); + + table_close(pkrel, NoLock); + + char *conname_addition = + ChooseForeignKeyConstraintNameAddition(numfks, confkey, parentRel->rd_id); + char *conname = ChooseConstraintName(get_rel_name(fk->conrelid), + conname_addition, + "fkey", + fk->connamespace, + NIL); + Oid conoid = CreateConstraintEntry(conname, + fk->connamespace, + CONSTRAINT_FOREIGN, + fk->condeferrable, + fk->condeferred, + fk->convalidated, + fk->oid, + fk->conrelid, + conkey, + numfks, + numfks, + InvalidOid, + indexoid, + chunk->table_id, + mapped_confkey, + conpfeqop, + conppeqop, + conffeqop, + numfks, + fk->confupdtype, + fk->confdeltype, +#if PG15_GE + confdelsetcols, + numfkdelsetcols, +#endif + fk->confmatchtype, + NULL, + NULL, + NULL, + false, + 1, + false, + false); + + ObjectAddress address, referenced; + ObjectAddressSet(address, ConstraintRelationId, conoid); + ObjectAddressSet(referenced, ConstraintRelationId, fk->oid); + recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL); + + CommandCounterIncrement(); + + createForeignKeyActionTriggers(fk, + fk->conrelid, + chunk->table_id, + conoid, + indexoid, + parentDelTrigger, + parentUpdTrigger); +} + +/* + * Generate the column-name portion of the constraint name for a new foreign + * key given the list of column names that reference the referenced + * table. This will be passed to ChooseConstraintName along with the parent + * table name and the "fkey" suffix. + * + * We know that less than NAMEDATALEN characters will actually be used, so we + * can truncate the result once we've generated that many. + * + * This function is based on a static function by the same name in tablecmds.c in PostgreSQL. + */ +static char * +ChooseForeignKeyConstraintNameAddition(int numkeys, AttrNumber *keys, Oid relid) +{ + char buf[NAMEDATALEN * 2]; + int buflen = 0; + + buf[0] = '\0'; + for (int i = 0; i < numkeys; i++) + { + char *name = get_attname(relid, keys[i], false); + if (buflen > 0) + buf[buflen++] = '_'; /* insert _ between names */ + + /* + * At this point we have buflen <= NAMEDATALEN. name should be less + * than NAMEDATALEN already, but use strlcpy for paranoia. + */ + strlcpy(buf + buflen, name, NAMEDATALEN); + buflen += strlen(buf + buflen); + if (buflen >= NAMEDATALEN) + break; + } + return pstrdup(buf); +} + +/* + * createForeignKeyActionTriggers + * Create the referenced-side "action" triggers that implement a foreign + * key. + * This function is based on a static function by the same name in tablecmds.c in PostgreSQL. + */ +static void +createForeignKeyActionTriggers(Form_pg_constraint fk, Oid relid, Oid refRelOid, Oid constraintOid, + Oid indexOid, Oid parentDelTrigger, Oid parentUpdTrigger) +{ + CreateTrigStmt *fk_trigger; + + /* + * Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON + * DELETE action on the referenced table. + */ + fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; + fk_trigger->trigname = "RI_ConstraintTrigger_a"; + fk_trigger->relation = NULL; + fk_trigger->args = NIL; + fk_trigger->row = true; + fk_trigger->timing = TRIGGER_TYPE_AFTER; + fk_trigger->events = TRIGGER_TYPE_DELETE; + fk_trigger->columns = NIL; + fk_trigger->whenClause = NULL; + fk_trigger->transitionRels = NIL; + fk_trigger->constrrel = NULL; + switch (fk->confdeltype) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fk->condeferrable; + fk_trigger->initdeferred = fk->condeferred; + 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) fk->confdeltype); + break; + } + + /* + * clang will complain here about swapped arguments but this is intentional + * as this is the reverse trigger from the referenced table back to the + * referencing table. So we disable that specific warning for the next call. + * + * NOLINTBEGIN(readability-suspicious-call-argument) + */ + CreateTrigger(fk_trigger, + NULL, + refRelOid, + relid, + constraintOid, + indexOid, + InvalidOid, + parentDelTrigger, + NULL, + true, + false); + /* NOLINTEND(readability-suspicious-call-argument) */ + + /* Make changes-so-far visible */ + CommandCounterIncrement(); + + /* + * Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON + * UPDATE action on the referenced table. + */ + fk_trigger = makeNode(CreateTrigStmt); + fk_trigger->replace = false; + fk_trigger->isconstraint = true; + fk_trigger->trigname = "RI_ConstraintTrigger_a"; + fk_trigger->relation = NULL; + fk_trigger->args = NIL; + fk_trigger->row = true; + fk_trigger->timing = TRIGGER_TYPE_AFTER; + fk_trigger->events = TRIGGER_TYPE_UPDATE; + fk_trigger->columns = NIL; + fk_trigger->whenClause = NULL; + fk_trigger->transitionRels = NIL; + fk_trigger->constrrel = NULL; + switch (fk->confupdtype) + { + case FKCONSTR_ACTION_NOACTION: + fk_trigger->deferrable = fk->condeferrable; + fk_trigger->initdeferred = fk->condeferred; + 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) fk->confupdtype); + break; + } + + /* + * clang will complain here about swapped arguments but this is intentional + * as this is the reverse trigger from the referenced table back to the + * referencing table. + * + * NOLINTBEGIN(readability-suspicious-call-argument) + */ + CreateTrigger(fk_trigger, + NULL, + refRelOid, + relid, + constraintOid, + indexOid, + InvalidOid, + parentUpdTrigger, + NULL, + true, + false); + /* NOLINTEND(readability-suspicious-call-argument) */ + + /* Make changes-so-far visible */ + CommandCounterIncrement(); +} + +/* + * Return a list of foreign key pg_constraint heap tuples referencing reloid. + */ +static List * +relation_get_referencing_fk(Oid reloid) +{ + List *result = NIL; + Relation conrel; + SysScanDesc conscan; + ScanKeyData skey[2]; + HeapTuple htup; + + /* Prepare to scan pg_constraint for entries having confrelid = this rel. */ + ScanKeyInit(&skey[0], + Anum_pg_constraint_confrelid, + BTEqualStrategyNumber, + F_OIDEQ, + ObjectIdGetDatum(reloid)); + + ScanKeyInit(&skey[1], + Anum_pg_constraint_contype, + BTEqualStrategyNumber, + F_CHAREQ, + CharGetDatum(CONSTRAINT_FOREIGN)); + + conrel = table_open(ConstraintRelationId, AccessShareLock); + conscan = systable_beginscan(conrel, InvalidOid, false, NULL, 2, skey); + + while (HeapTupleIsValid(htup = systable_getnext(conscan))) + { + result = lappend(result, heap_copytuple(htup)); + } + + systable_endscan(conscan); + table_close(conrel, AccessShareLock); + + return result; +} + +/* + * Return a list of foreign key pg_constraint heap tuples referencing reloid. + */ +static HeapTuple +relation_get_fk_constraint(Oid conrelid, Oid confrelid) +{ + Relation conrel; + SysScanDesc conscan; + ScanKeyData skey[3]; + HeapTuple htup = NULL; + + /* Prepare to scan pg_constraint for entries having confrelid = this rel. */ + ScanKeyInit(&skey[0], + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, + F_OIDEQ, + ObjectIdGetDatum(conrelid)); + + ScanKeyInit(&skey[1], + Anum_pg_constraint_confrelid, + BTEqualStrategyNumber, + F_OIDEQ, + ObjectIdGetDatum(confrelid)); + + ScanKeyInit(&skey[2], + Anum_pg_constraint_contype, + BTEqualStrategyNumber, + F_CHAREQ, + CharGetDatum(CONSTRAINT_FOREIGN)); + + conrel = table_open(ConstraintRelationId, AccessShareLock); + conscan = systable_beginscan(conrel, InvalidOid, false, NULL, 3, skey); + + if (HeapTupleIsValid(htup = systable_getnext(conscan))) + { + htup = heap_copytuple(htup); + } + + systable_endscan(conscan); + table_close(conrel, AccessShareLock); + + return htup; +} + +/* Get the UPDATE and DELETE trigger OIDs for the given constraint OID */ +static void +constraint_get_trigger(Oid conoid, Oid *updtrigoid, Oid *deltrigoid) +{ + Relation rel; + SysScanDesc scan; + ScanKeyData skey[1]; + HeapTuple htup; + + *updtrigoid = InvalidOid; + *deltrigoid = InvalidOid; + + ScanKeyInit(&skey[0], + Anum_pg_trigger_tgconstraint, + BTEqualStrategyNumber, + F_OIDEQ, + ObjectIdGetDatum(conoid)); + + rel = table_open(TriggerRelationId, AccessShareLock); + scan = systable_beginscan(rel, TriggerConstraintIndexId, true, NULL, 1, skey); + + while (HeapTupleIsValid(htup = systable_getnext(scan))) + { + Form_pg_trigger trigform = (Form_pg_trigger) GETSTRUCT(htup); + + if ((trigform->tgtype & TRIGGER_TYPE_UPDATE) == TRIGGER_TYPE_UPDATE) + *updtrigoid = trigform->oid; + if ((trigform->tgtype & TRIGGER_TYPE_DELETE) == TRIGGER_TYPE_DELETE) + *deltrigoid = trigform->oid; + } + + systable_endscan(scan); + table_close(rel, AccessShareLock); +} + +/* + * Return the oid of the index supporting the foreign key constraint. + */ +static Oid +get_fk_index(Relation rel, int nkeys, AttrNumber *confkeys) +{ + Oid indexoid = InvalidOid; + List *indexes = RelationGetIndexList(rel); + ListCell *lc; + + foreach (lc, indexes) + { + Oid indexoid = lfirst_oid(lc); + Relation indexrel = index_open(indexoid, AccessShareLock); + bool match = true; + + if (!indexrel->rd_index->indisunique || indexrel->rd_index->indnkeyatts != nkeys) + { + index_close(indexrel, AccessShareLock); + continue; + } + + for (int i = 0; i < nkeys; i++) + { + if (indexrel->rd_index->indkey.values[i] != confkeys[i]) + { + match = false; + } + } + + index_close(indexrel, AccessShareLock); + if (match) + { + return indexoid; + } + } + + return indexoid; +} diff --git a/src/foreign_key.h b/src/foreign_key.h new file mode 100644 index 00000000000..e5d6cae1063 --- /dev/null +++ b/src/foreign_key.h @@ -0,0 +1,16 @@ +/* + * This file and its contents are licensed under the Apache License 2.0. + * Please see the included NOTICE for copyright information and + * LICENSE-APACHE for a copy of the license. + */ + +#include +#include +#include + +#include "chunk.h" +#include "export.h" +#include "hypertable.h" + +extern TSDLLEXPORT void ts_fk_propagate(Oid conrelid, Hypertable *ht); +extern TSDLLEXPORT void ts_chunk_copy_referencing_fk(const Hypertable *ht, const Chunk *chunk); diff --git a/src/hypertable.c b/src/hypertable.c index 25465e27b0e..fc5e27f45d3 100644 --- a/src/hypertable.c +++ b/src/hypertable.c @@ -1276,6 +1276,15 @@ hypertable_validate_constraints(Oid relid) { Form_pg_constraint form = (Form_pg_constraint) GETSTRUCT(tuple); + if (form->contype == CONSTRAINT_FOREIGN) + { + if (ts_hypertable_relid_to_id(form->confrelid) != -1) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypertables cannot be used as foreign key references of " + "hypertables"))); + } + if (form->contype == CONSTRAINT_CHECK && form->connoinherit) ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), diff --git a/src/planner/planner.c b/src/planner/planner.c index 2ed58cfcaf6..f22537ebc8f 100644 --- a/src/planner/planner.c +++ b/src/planner/planner.c @@ -383,6 +383,71 @@ preprocess_query(Node *node, PreprocessQueryContext *context) Index rti = 1; bool ret; + /* + * Detect FOREIGN KEY lookup queries and mark the RTE for expansion. + * Unfortunately postgres will create lookup queries for foreign keys + * with `ONLY` preventing hypertable expansion. Only for declarative + * partitioned tables the queries will be created without `ONLY`. + * We try to detect these queries here and undo the `ONLY` flag for + * these specific queries. + * + * The implementation of this on the postgres side can be found in + * src/backend/utils/adt/ri_triggers.c + */ + + /* + * RI_FKey_check + * + * The RI_FKey_check query string built is + * SELECT 1 FROM [ONLY] x WHERE pkatt1 = $1 [AND ...] + * FOR KEY SHARE OF x + */ + if (query->commandType == CMD_SELECT && query->hasForUpdate && + list_length(query->rtable) == 1 && context->root->glob->boundParams) + { + RangeTblEntry *rte = linitial_node(RangeTblEntry, query->rtable); + if (!rte->inh && rte->rtekind == RTE_RELATION && rte->rellockmode == RowShareLock && + list_length(query->jointree->fromlist) == 1 && query->jointree->quals && + strcmp(rte->eref->aliasname, "x") == 0) + { + Hypertable *ht = + ts_hypertable_cache_get_entry(hcache, rte->relid, CACHE_FLAG_MISSING_OK); + if (ht) + { + rte->inh = true; + if (TS_HYPERTABLE_HAS_COMPRESSION_ENABLED(ht)) + query->rowMarks = NIL; + } + } + } + /* + * RI_Initial_Check query + * + * The RI_Initial_Check query string built is: + * SELECT fk.keycols FROM [ONLY] relname fk + * LEFT OUTER JOIN [ONLY] pkrelname pk + * ON (pk.pkkeycol1=fk.keycol1 [AND ...]) + * WHERE pk.pkkeycol1 IS NULL AND + * For MATCH SIMPLE: + * (fk.keycol1 IS NOT NULL [AND ...]) + * For MATCH FULL: + * (fk.keycol1 IS NOT NULL [OR ...]) + */ + if (query->commandType == CMD_SELECT && list_length(query->rtable) == 3) + { + RangeTblEntry *rte1 = linitial_node(RangeTblEntry, query->rtable); + RangeTblEntry *rte2 = lsecond_node(RangeTblEntry, query->rtable); + if (!rte1->inh && !rte2->inh && rte1->rtekind == RTE_RELATION && + rte2->rtekind == RTE_RELATION && strcmp(rte1->eref->aliasname, "fk") == 0 && + strcmp(rte2->eref->aliasname, "pk") == 0) + { + if (ts_hypertable_cache_get_entry(hcache, rte1->relid, CACHE_FLAG_MISSING_OK)) + rte1->inh = true; + if (ts_hypertable_cache_get_entry(hcache, rte2->relid, CACHE_FLAG_MISSING_OK)) + rte2->inh = true; + } + } + foreach (lc, query->rtable) { RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); diff --git a/src/process_utility.c b/src/process_utility.c index 72cabb29925..575bd177bc9 100644 --- a/src/process_utility.c +++ b/src/process_utility.c @@ -3,8 +3,8 @@ * Please see the included NOTICE for copyright information and * LICENSE-APACHE for a copy of the license. */ -#include +#include #include #include #include @@ -56,6 +56,7 @@ #include "export.h" #include "extension.h" #include "extension_constants.h" +#include "foreign_key.h" #include "hypercube.h" #include "hypertable.h" #include "hypertable_cache.h" @@ -2247,35 +2248,6 @@ process_altertable_drop_column(Hypertable *ht, AlterTableCmd *cmd) } } -/* process all regular-table alter commands to make sure they aren't adding - * foreign-key constraints to hypertables */ -static void -verify_constraint_plaintable(RangeVar *relation, Constraint *constr) -{ - Cache *hcache; - Hypertable *ht; - - Assert(IsA(constr, Constraint)); - - hcache = ts_hypertable_cache_pin(); - - switch (constr->contype) - { - case CONSTR_FOREIGN: - ht = ts_hypertable_cache_get_entry_rv(hcache, constr->pktable); - - if (NULL != ht) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("foreign keys to hypertables are not supported"))); - break; - default: - break; - } - - ts_cache_release(hcache); -} - /* * Verify that a constraint is supported on a hypertable. */ @@ -2294,6 +2266,16 @@ verify_constraint_hypertable(Hypertable *ht, Node *constr_node) keys = (contype == CONSTR_EXCLUSION) ? constr->exclusions : constr->keys; indexname = constr->indexname; + if (contype == CONSTR_FOREIGN) + { + Oid confrelid = ts_hypertable_relid(constr->pktable); + if (OidIsValid(confrelid)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypertables cannot be used as foreign key references of " + "hypertables"))); + } + /* NO INHERIT constraints do not really make sense on a hypertable */ if (constr->is_no_inherit) ereport(ERROR, @@ -2345,9 +2327,7 @@ verify_constraint(RangeVar *relation, Constraint *constr) Cache *hcache = ts_hypertable_cache_pin(); Hypertable *ht = ts_hypertable_cache_get_entry_rv(hcache, relation); - if (NULL == ht) - verify_constraint_plaintable(relation, constr); - else + if (ht) verify_constraint_hypertable(ht, (Node *) constr); ts_cache_release(hcache); @@ -3019,9 +2999,6 @@ process_cluster_start(ProcessUtilityArgs *args) /* * Process create table statements. * - * For regular tables, we need to ensure that they don't have any foreign key - * constraints that point to hypertables. - * * NOTE that this function should be called after parse analysis (in an end DDL * trigger or by running parse analysis manually). */ @@ -3368,10 +3345,7 @@ process_altertable_start_table(ProcessUtilityArgs *args) col = (ColumnDef *) cmd->def; if (ht && TS_HYPERTABLE_HAS_COMPRESSION_ENABLED(ht)) check_altertable_add_column_for_compressed(ht, col); - if (NULL == ht) - foreach (constraint_lc, col->constraints) - verify_constraint_plaintable(stmt->relation, lfirst(constraint_lc)); - else + if (ht) foreach (constraint_lc, col->constraints) verify_constraint_hypertable(ht, lfirst(constraint_lc)); break; @@ -3380,7 +3354,7 @@ process_altertable_start_table(ProcessUtilityArgs *args) #if PG16_LT case AT_DropColumnRecurse: #endif - if (NULL != ht) + if (ht) process_altertable_drop_column(ht, cmd); break; case AT_AddConstraint: @@ -3389,15 +3363,13 @@ process_altertable_start_table(ProcessUtilityArgs *args) #endif Assert(IsA(cmd->def, Constraint)); - if (NULL == ht) - verify_constraint_plaintable(stmt->relation, (Constraint *) cmd->def); - else + if (ht) verify_constraint_hypertable(ht, cmd->def); break; case AT_AlterColumnType: Assert(IsA(cmd->def, ColumnDef)); - if (ht != NULL) + if (ht) process_alter_column_type_start(ht, cmd); break; case AT_AttachPartition: @@ -3407,7 +3379,7 @@ process_altertable_start_table(ProcessUtilityArgs *args) partstmt = (PartitionCmd *) cmd->def; relation = partstmt->name; - Assert(NULL != relation); + Assert(relation); if (OidIsValid(ts_hypertable_relid(relation))) { @@ -3875,7 +3847,7 @@ process_altertable_end_table(Node *parsetree, CollectedCommand *cmd) ht = ts_hypertable_cache_get_cache_and_entry(relid, CACHE_FLAG_MISSING_OK, &hcache); - if (NULL != ht) + if (ht) { switch (cmd->type) { @@ -3889,6 +3861,40 @@ process_altertable_end_table(Node *parsetree, CollectedCommand *cmd) break; } } + + /* + * Check any ALTER TABLE command is adding a FOREIGN KEY constraint + * referencing a hypertable. + */ + if (cmd->type == SCT_AlterTable) + { + AlterTableStmt *stmt = castNode(AlterTableStmt, parsetree); + ListCell *lc; + + foreach (lc, stmt->cmds) + { + AlterTableCmd *subcmd = (AlterTableCmd *) lfirst(lc); + + if (subcmd->subtype != AT_AddConstraint || + castNode(Constraint, subcmd->def)->contype != CONSTR_FOREIGN) + continue; + + Constraint *c = castNode(Constraint, subcmd->def); + Oid confrelid = RangeVarGetRelid(c->pktable, AccessShareLock, true); + Hypertable *pk = + ts_hypertable_cache_get_entry(hcache, confrelid, CACHE_FLAG_MISSING_OK); + if (pk) + { + if (ht) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("hypertables cannot be used as foreign key references of " + "hypertables"))); + ts_fk_propagate(relid, pk); + } + } + } + ts_cache_release(hcache); } diff --git a/test/expected/constraint.out b/test/expected/constraint.out index b22baab9360..ec6ecce7204 100644 --- a/test/expected/constraint.out +++ b/test/expected/constraint.out @@ -413,7 +413,7 @@ INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES --delete should fail \set ON_ERROR_STOP 0 DELETE FROM devices; -ERROR: update or delete on table "devices" violates foreign key constraint "8_19_hyper_fk_device_id_fkey" on table "_hyper_4_8_chunk" +ERROR: update or delete on table "devices" violates foreign key constraint "hyper_fk_device_id_fkey" on table "hyper_fk" \set ON_ERROR_STOP 1 ALTER TABLE hyper_fk DROP CONSTRAINT hyper_fk_device_id_fkey; --should now be able to add non-fk rows @@ -423,7 +423,7 @@ INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES \set ON_ERROR_STOP 0 ALTER TABLE hyper_fk ADD CONSTRAINT hyper_fk_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(device_id); -ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_21_hyper_fk_device_id_fkey" +ERROR: insert or update on table "hyper_fk" violates foreign key constraint "hyper_fk_device_id_fkey" \set ON_ERROR_STOP 1 --but can add a NOT VALID one ALTER TABLE hyper_fk ADD CONSTRAINT hyper_fk_device_id_fkey @@ -431,7 +431,7 @@ FOREIGN KEY (device_id) REFERENCES devices(device_id) NOT VALID; --which will fail when validated \set ON_ERROR_STOP 0 ALTER TABLE hyper_fk VALIDATE CONSTRAINT hyper_fk_device_id_fkey; -ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_22_hyper_fk_device_id_fkey" +ERROR: insert or update on table "hyper_fk" violates foreign key constraint "hyper_fk_device_id_fkey" \set ON_ERROR_STOP 1 ALTER TABLE hyper_fk DROP CONSTRAINT hyper_fk_device_id_fkey; DELETE FROM hyper_fk WHERE device_id = 'dev3'; @@ -440,13 +440,13 @@ FOREIGN KEY (device_id) REFERENCES devices(device_id); \set ON_ERROR_STOP 0 INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES (1257987700000000002, 'dev3', 11); -ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_23_hyper_fk_device_id_fkey" +ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_22_hyper_fk_device_id_fkey" \set ON_ERROR_STOP 1 SELECT * FROM test.show_constraints('_timescaledb_internal._hyper_4_8_chunk'); Constraint | Type | Columns | Index | Expr | Deferrable | Deferred | Validated ------------------------------+------+-------------+--------------------------------------------+------------------------------------------------------------------------------------------+------------+----------+----------- 8_20_hyper_fk_pkey | p | {time} | _timescaledb_internal."8_20_hyper_fk_pkey" | | f | f | t - 8_23_hyper_fk_device_id_fkey | f | {device_id} | devices_pkey | | f | f | t + 8_22_hyper_fk_device_id_fkey | f | {device_id} | devices_pkey | | f | f | t constraint_8 | c | {time} | - | (("time" >= '1257987700000000000'::bigint) AND ("time" < '1257987700000000010'::bigint)) | f | f | t hyper_fk_sensor_1_check | c | {sensor_1} | - | (sensor_1 > (10)::numeric) | f | f | t (4 rows) @@ -463,7 +463,7 @@ SELECT * FROM _timescaledb_catalog.chunk_constraint; 6 | | 6_16_hyper_pk_pkey | hyper_pk_pkey 8 | 8 | constraint_8 | 8 | | 8_20_hyper_fk_pkey | hyper_fk_pkey - 8 | | 8_23_hyper_fk_device_id_fkey | hyper_fk_device_id_fkey + 8 | | 8_22_hyper_fk_device_id_fkey | hyper_fk_device_id_fkey (10 rows) --test CASCADE drop behavior @@ -513,7 +513,7 @@ BEGIN; (1 row) COMMIT; -ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_24_hyper_fk_device_id_fkey" +ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_23_hyper_fk_device_id_fkey" \set ON_ERROR_STOP 1 ALTER TABLE hyper_fk ALTER CONSTRAINT hyper_fk_device_id_fkey NOT DEFERRABLE; \set ON_ERROR_STOP 0 @@ -521,7 +521,7 @@ BEGIN; --error detected right away INSERT INTO hyper_fk(time, device_id,sensor_1) VALUES (1257987700000000003, 'dev4', 11); -ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_24_hyper_fk_device_id_fkey" +ERROR: insert or update on table "_hyper_4_8_chunk" violates foreign key constraint "8_23_hyper_fk_device_id_fkey" SELECT 1; ERROR: current transaction is aborted, commands ignored until end of transaction block COMMIT; @@ -548,7 +548,6 @@ SELECT * FROM create_hypertable('hyper_for_ref', 'time', chunk_time_interval => CREATE TABLE referrer ( time BIGINT NOT NULL REFERENCES hyper_for_ref(time) ); -ERROR: foreign keys to hypertables are not supported \set ON_ERROR_STOP 1 CREATE TABLE referrer2 ( time BIGINT NOT NULL @@ -556,7 +555,6 @@ CREATE TABLE referrer2 ( \set ON_ERROR_STOP 0 ALTER TABLE referrer2 ADD CONSTRAINT hyper_fk_device_id_fkey FOREIGN KEY (time) REFERENCES hyper_for_ref(time); -ERROR: foreign keys to hypertables are not supported \set ON_ERROR_STOP 1 ----------------------- EXCLUSION CONSTRAINT ------------------ CREATE TABLE hyper_ex ( @@ -580,7 +578,7 @@ INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES \set ON_ERROR_STOP 0 INSERT INTO hyper_ex(time, device_id,sensor_1) VALUES (1257987700000000000, 'dev2', 12); -ERROR: conflicting key value violates exclusion constraint "9_26_hyper_ex_time_device_id_excl" +ERROR: conflicting key value violates exclusion constraint "9_25_hyper_ex_time_device_id_excl" \set ON_ERROR_STOP 1 ALTER TABLE hyper_ex DROP CONSTRAINT hyper_ex_time_device_id_excl; --can now add @@ -593,7 +591,7 @@ ALTER TABLE hyper_ex ADD CONSTRAINT hyper_ex_time_device_id_excl time WITH =, device_id WITH = ) WHERE (not canceled) ; -ERROR: could not create exclusion constraint "9_27_hyper_ex_time_device_id_excl" +ERROR: could not create exclusion constraint "9_26_hyper_ex_time_device_id_excl" \set ON_ERROR_STOP 1 DELETE FROM hyper_ex WHERE sensor_1 = 12; ALTER TABLE hyper_ex ADD CONSTRAINT hyper_ex_time_device_id_excl @@ -613,7 +611,7 @@ BEGIN; (1 row) COMMIT; -ERROR: conflicting key value violates exclusion constraint "9_28_hyper_ex_time_device_id_excl" +ERROR: conflicting key value violates exclusion constraint "9_27_hyper_ex_time_device_id_excl" \set ON_ERROR_STOP 1 --cannot add exclusion constraint without partition key. CREATE TABLE hyper_ex_invalid ( @@ -684,7 +682,7 @@ BEGIN; (1 row) COMMIT; -ERROR: duplicate key value violates unique constraint "10_29_hyper_unique_deferred_time_key" +ERROR: duplicate key value violates unique constraint "10_28_hyper_unique_deferred_time_key" \set ON_ERROR_STOP 1 --test deferred on create table CREATE TABLE hyper_pk_deferred ( @@ -710,7 +708,7 @@ BEGIN; (1 row) COMMIT; -ERROR: duplicate key value violates unique constraint "11_30_hyper_pk_deferred_pkey" +ERROR: duplicate key value violates unique constraint "11_29_hyper_pk_deferred_pkey" \set ON_ERROR_STOP 1 --test that deferred works on create table too CREATE TABLE hyper_fk_deferred ( @@ -735,7 +733,7 @@ BEGIN; (1 row) COMMIT; -ERROR: insert or update on table "_hyper_11_12_chunk" violates foreign key constraint "12_31_hyper_fk_deferred_device_id_fkey" +ERROR: insert or update on table "_hyper_11_12_chunk" violates foreign key constraint "12_30_hyper_fk_deferred_device_id_fkey" \set ON_ERROR_STOP 1 CREATE TABLE hyper_ex_deferred ( time BIGINT, @@ -765,7 +763,7 @@ BEGIN; (1 row) COMMIT; -ERROR: conflicting key value violates exclusion constraint "13_34_hyper_ex_deferred_time_device_id_excl" +ERROR: conflicting key value violates exclusion constraint "13_33_hyper_ex_deferred_time_device_id_excl" \set ON_ERROR_STOP 1 -- Make sure renaming schemas won't break dropping constraints \c :TEST_DBNAME :ROLE_SUPERUSER diff --git a/test/expected/create_table.out b/test/expected/create_table.out index d828289caa2..8157265c4f3 100644 --- a/test/expected/create_table.out +++ b/test/expected/create_table.out @@ -14,7 +14,6 @@ SELECT create_hypertable('test_hyper_pk', 'time'); \set ON_ERROR_STOP 0 -- Foreign key constraints that reference hypertables are currently unsupported CREATE TABLE test_fk(time TIMESTAMPTZ REFERENCES test_hyper_pk(time)); -ERROR: foreign keys to hypertables are not supported \set ON_ERROR_STOP 1 CREATE TABLE test_delete(time timestamp with time zone PRIMARY KEY, temp float); SELECT create_hypertable('test_delete', 'time'); diff --git a/test/expected/rowsecurity-14.out b/test/expected/rowsecurity-14.out index 3c95b64c662..9e4e4484fdf 100644 --- a/test/expected/rowsecurity-14.out +++ b/test/expected/rowsecurity-14.out @@ -542,8 +542,8 @@ SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid ORDER BY d. (6 rows) DELETE FROM category WHERE cid = 33; -- fails with FK violation -ERROR: update or delete on table "category" violates foreign key constraint "4_7_document_cid_fkey" on table "_hyper_1_4_chunk" -DETAIL: Key is still referenced from table "_hyper_1_4_chunk". +ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document" +DETAIL: Key is still referenced from table "document". -- can insert FK referencing invisible PK SET SESSION AUTHORIZATION regress_rls_carol; SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid ORDER BY d.did, c.cid; @@ -4715,8 +4715,8 @@ ALTER TABLE r2 ENABLE ROW LEVEL SECURITY; ALTER TABLE r2 FORCE ROW LEVEL SECURITY; -- Errors due to rows in r2 DELETE FROM r1; -ERROR: update or delete on table "r1" violates foreign key constraint "113_23_r2_a_fkey" on table "_hyper_26_113_chunk" -DETAIL: Key (a)=(10) is still referenced from table "_hyper_26_113_chunk". +ERROR: update or delete on table "r1" violates foreign key constraint "r2_a_fkey" on table "r2" +DETAIL: Key (a)=(10) is still referenced from table "r2". -- Reset r2 to no-RLS DROP POLICY p1 ON r2; ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY; diff --git a/test/expected/rowsecurity-15.out b/test/expected/rowsecurity-15.out index e2ce06ccb2a..ad13d7b7412 100644 --- a/test/expected/rowsecurity-15.out +++ b/test/expected/rowsecurity-15.out @@ -542,8 +542,8 @@ SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid ORDER BY d. (6 rows) DELETE FROM category WHERE cid = 33; -- fails with FK violation -ERROR: update or delete on table "category" violates foreign key constraint "4_7_document_cid_fkey" on table "_hyper_1_4_chunk" -DETAIL: Key is still referenced from table "_hyper_1_4_chunk". +ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document" +DETAIL: Key is still referenced from table "document". -- can insert FK referencing invisible PK SET SESSION AUTHORIZATION regress_rls_carol; SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid ORDER BY d.did, c.cid; @@ -4717,8 +4717,8 @@ ALTER TABLE r2 ENABLE ROW LEVEL SECURITY; ALTER TABLE r2 FORCE ROW LEVEL SECURITY; -- Errors due to rows in r2 DELETE FROM r1; -ERROR: update or delete on table "r1" violates foreign key constraint "113_23_r2_a_fkey" on table "_hyper_26_113_chunk" -DETAIL: Key (a)=(10) is still referenced from table "_hyper_26_113_chunk". +ERROR: update or delete on table "r1" violates foreign key constraint "r2_a_fkey" on table "r2" +DETAIL: Key (a)=(10) is still referenced from table "r2". -- Reset r2 to no-RLS DROP POLICY p1 ON r2; ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY; diff --git a/test/expected/rowsecurity-16.out b/test/expected/rowsecurity-16.out index e2ce06ccb2a..ad13d7b7412 100644 --- a/test/expected/rowsecurity-16.out +++ b/test/expected/rowsecurity-16.out @@ -542,8 +542,8 @@ SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid ORDER BY d. (6 rows) DELETE FROM category WHERE cid = 33; -- fails with FK violation -ERROR: update or delete on table "category" violates foreign key constraint "4_7_document_cid_fkey" on table "_hyper_1_4_chunk" -DETAIL: Key is still referenced from table "_hyper_1_4_chunk". +ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document" +DETAIL: Key is still referenced from table "document". -- can insert FK referencing invisible PK SET SESSION AUTHORIZATION regress_rls_carol; SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid ORDER BY d.did, c.cid; @@ -4717,8 +4717,8 @@ ALTER TABLE r2 ENABLE ROW LEVEL SECURITY; ALTER TABLE r2 FORCE ROW LEVEL SECURITY; -- Errors due to rows in r2 DELETE FROM r1; -ERROR: update or delete on table "r1" violates foreign key constraint "113_23_r2_a_fkey" on table "_hyper_26_113_chunk" -DETAIL: Key (a)=(10) is still referenced from table "_hyper_26_113_chunk". +ERROR: update or delete on table "r1" violates foreign key constraint "r2_a_fkey" on table "r2" +DETAIL: Key (a)=(10) is still referenced from table "r2". -- Reset r2 to no-RLS DROP POLICY p1 ON r2; ALTER TABLE r2 NO FORCE ROW LEVEL SECURITY; diff --git a/tsl/src/nodes/decompress_chunk/decompress_chunk.c b/tsl/src/nodes/decompress_chunk/decompress_chunk.c index 94e5f79b92d..4edda1e0dfe 100644 --- a/tsl/src/nodes/decompress_chunk/decompress_chunk.c +++ b/tsl/src/nodes/decompress_chunk/decompress_chunk.c @@ -1682,7 +1682,7 @@ decompress_chunk_add_plannerinfo(PlannerInfo *root, CompressionInfo *info, Chunk expand_planner_arrays(root, 1); info->compressed_rte = - decompress_chunk_make_rte(compressed_reloid, AccessShareLock, root->parse); + decompress_chunk_make_rte(compressed_reloid, info->chunk_rte->rellockmode, root->parse); root->simple_rte_array[compressed_index] = info->compressed_rte; root->parse->rtable = lappend(root->parse->rtable, info->compressed_rte); diff --git a/tsl/src/planner.c b/tsl/src/planner.c index 91062604d6d..798529486b3 100644 --- a/tsl/src/planner.c +++ b/tsl/src/planner.c @@ -91,15 +91,7 @@ tsl_set_rel_pathlist_query(PlannerInfo *root, RelOptInfo *rel, Index rti, RangeT * 2) if it is not a SELECT QUERY. * Caching is done by our hypertable expansion, which doesn't run in * these cases. - * - * Also on PG13 when a DELETE query runs through SPI, its command - * type is CMD_SELECT. Apparently it goes into inheritance_planner, - * which uses a hack to pretend it's actually a SELECT query, but - * for some reason for non-SPI queries the query type is still - * correct. You can observe it in the continuous_aggs-13 test. - * Just ignore this assertion on 13 and look up the chunk. */ - Assert(rel->reloptkind == RELOPT_BASEREL || root->parse->commandType != CMD_SELECT); fdw_private->cached_chunk_struct = ts_chunk_get_by_relid(rte->relid, /* fail_if_not_found = */ true); } diff --git a/tsl/test/expected/foreign_keys.out b/tsl/test/expected/foreign_keys.out new file mode 100644 index 00000000000..416d287a94e --- /dev/null +++ b/tsl/test/expected/foreign_keys.out @@ -0,0 +1,582 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. +-- test single column fk constraint from plain table to hypertable during hypertable creation +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); +CREATE TABLE event(time timestamptz references metrics(time), info text); +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; +\set ON_ERROR_STOP 0 +-- should fail +DELETE FROM metrics WHERE time = '2020-01-01'; +ERROR: update or delete on table "_hyper_1_1_chunk" violates foreign key constraint "event_time_fkey1" on table "event" +-- should fail +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +ERROR: update or delete on table "_hyper_1_1_chunk" violates foreign key constraint "event_time_fkey1" on table "event" +\set ON_ERROR_STOP 1 +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_1_1_chunk | t +(2 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +------------------------+--------+--------------+--------------- + "RI_FKey_noaction_del" | f | t | event + "RI_FKey_noaction_upd" | f | t | event + "RI_FKey_noaction_del" | t | t | event + "RI_FKey_noaction_upd" | t | t | event +(4 rows) + +-- create new chunk and repeat the test +INSERT INTO metrics(time, device, value) VALUES ('2021-01-01', 'd1', 1.0); +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2021-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2021-01-01', 'info2'); +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_1_1_chunk | t + event_time_fkey2 | event | _timescaledb_internal._hyper_1_2_chunk | t +(3 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +------------------------+--------+--------------+--------------- + "RI_FKey_noaction_del" | f | t | event + "RI_FKey_noaction_upd" | f | t | event + "RI_FKey_noaction_del" | t | t | event + "RI_FKey_noaction_upd" | t | t | event + "RI_FKey_noaction_del" | t | t | event + "RI_FKey_noaction_upd" | t | t | event +(6 rows) + +-- chunks referenced in fk constraints must not be dropped or truncated +\set ON_ERROR_STOP 0 +TRUNCATE metrics; +ERROR: cannot truncate a table referenced in a foreign key constraint +TRUNCATE _timescaledb_internal._hyper_1_1_chunk; +ERROR: cannot truncate a table referenced in a foreign key constraint +TRUNCATE _timescaledb_internal._hyper_1_2_chunk; +ERROR: cannot truncate a table referenced in a foreign key constraint +DROP TABLE _timescaledb_internal._hyper_1_1_chunk; +ERROR: cannot drop table _timescaledb_internal._hyper_1_1_chunk because other objects depend on it +DROP TABLE _timescaledb_internal._hyper_1_2_chunk; +ERROR: cannot drop table _timescaledb_internal._hyper_1_2_chunk because other objects depend on it +SELECT drop_chunks('metrics', '1 month'::interval); +ERROR: cannot drop constraint 1_1_metrics_pkey on table _timescaledb_internal._hyper_1_1_chunk because other objects depend on it +\set ON_ERROR_STOP 1 +-- after removing constraint dropping should succeed +ALTER TABLE event DROP CONSTRAINT event_time_fkey; +SELECT drop_chunks('metrics', '1 month'::interval); + drop_chunks +---------------------------------------- + _timescaledb_internal._hyper_1_1_chunk + _timescaledb_internal._hyper_1_2_chunk +(2 rows) + +DROP TABLE event; +DROP TABLE metrics; +-- test single column fk constraint from plain table to hypertable during hypertable creation with RESTRICT +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE RESTRICT ON UPDATE RESTRICT, info text); +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; +\set ON_ERROR_STOP 0 +-- should fail +DELETE FROM metrics WHERE time = '2020-01-01'; +ERROR: update or delete on table "_hyper_2_3_chunk" violates foreign key constraint "event_time_fkey1" on table "event" +-- should fail +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +ERROR: update or delete on table "_hyper_2_3_chunk" violates foreign key constraint "event_time_fkey1" on table "event" +\set ON_ERROR_STOP 1 +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_2_3_chunk | t +(2 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +------------------------+--------+--------------+--------------- + "RI_FKey_restrict_del" | f | t | event + "RI_FKey_restrict_upd" | f | t | event + "RI_FKey_restrict_del" | t | t | event + "RI_FKey_restrict_upd" | t | t | event +(4 rows) + +-- create new chunk and repeat the test +INSERT INTO metrics(time, device, value) VALUES ('2021-01-01', 'd1', 1.0); +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2021-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2021-01-01', 'info2'); +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_2_3_chunk | t + event_time_fkey2 | event | _timescaledb_internal._hyper_2_4_chunk | t +(3 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +------------------------+--------+--------------+--------------- + "RI_FKey_restrict_del" | f | t | event + "RI_FKey_restrict_upd" | f | t | event + "RI_FKey_restrict_del" | t | t | event + "RI_FKey_restrict_upd" | t | t | event + "RI_FKey_restrict_del" | t | t | event + "RI_FKey_restrict_upd" | t | t | event +(6 rows) + +DROP TABLE event; +DROP TABLE metrics; +-- test single column fk constraint from plain table to hypertable during hypertable creation with CASCADE +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE CASCADE ON UPDATE CASCADE, info text); +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; +-- should cascade +BEGIN; +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; + time | info +------+------ +(0 rows) + +ROLLBACK; +-- should cascade +BEGIN; +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; + time | info +------------------------------+------- + Wed Jan 01 00:30:00 2020 PST | info2 +(1 row) + +ROLLBACK; +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_3_5_chunk | t +(2 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +-----------------------+--------+--------------+--------------- + "RI_FKey_cascade_del" | f | t | event + "RI_FKey_cascade_upd" | f | t | event + "RI_FKey_cascade_del" | t | t | event + "RI_FKey_cascade_upd" | t | t | event +(4 rows) + +DROP TABLE event; +DROP TABLE metrics; +-- test single column fk constraint from plain table to hypertable during hypertable creation with SET NULL +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE SET NULL ON UPDATE SET NULL, info text); +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; +-- should cascade +BEGIN; +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; + time | info +------+------- + | info2 +(1 row) + +ROLLBACK; +-- should cascade +BEGIN; +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; + time | info +------+------- + | info2 +(1 row) + +ROLLBACK; +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_4_6_chunk | t +(2 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +-----------------------+--------+--------------+--------------- + "RI_FKey_setnull_del" | f | t | event + "RI_FKey_setnull_upd" | f | t | event + "RI_FKey_setnull_del" | t | t | event + "RI_FKey_setnull_upd" | t | t | event +(4 rows) + +DROP TABLE event; +DROP TABLE metrics; +-- test single column fk constraint from plain table to hypertable during hypertable creation with SET DEFAULT +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); +CREATE TABLE event(time timestamptz default null references metrics(time) ON DELETE SET DEFAULT ON UPDATE SET DEFAULT, info text); +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; +-- should cascade +BEGIN; +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; + time | info +------+------- + | info2 +(1 row) + +ROLLBACK; +-- should cascade +BEGIN; +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; + time | info +------+------- + | info2 +(1 row) + +ROLLBACK; +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_5_7_chunk | t +(2 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +--------------------------+--------+--------------+--------------- + "RI_FKey_setdefault_del" | f | t | event + "RI_FKey_setdefault_upd" | f | t | event + "RI_FKey_setdefault_del" | t | t | event + "RI_FKey_setdefault_upd" | t | t | event +(4 rows) + +DROP TABLE event; +DROP TABLE metrics; +-- test single column fk constraint from plain table to hypertable with constraint being added separately +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0); +CREATE TABLE event(time timestamptz, info text); +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time) ON DELETE RESTRICT; +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +-- should fail +\set ON_ERROR_STOP 0 +DELETE FROM metrics WHERE time = '2020-01-01'; +ERROR: update or delete on table "_hyper_6_8_chunk" violates foreign key constraint "event_time_fkey1" on table "event" +\set ON_ERROR_STOP 1 +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_6_8_chunk | t +(2 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +------------------------+--------+--------------+--------------- + "RI_FKey_restrict_del" | f | t | event + "RI_FKey_noaction_upd" | f | t | event + "RI_FKey_restrict_del" | t | t | event + "RI_FKey_noaction_upd" | t | t | event +(4 rows) + +DROP TABLE event; +DROP TABLE metrics; +-- test multi column fk constraint from plain table to hypertable +CREATE TABLE metrics(time timestamptz , device text, value float, primary key (time, device)); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0); +CREATE TABLE event(time timestamptz, device text, info text); +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time,device) REFERENCES metrics(time,device) ON DELETE RESTRICT; +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, device, info) VALUES ('2020-01-02', 'd1', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +INSERT INTO event(time, device, info) VALUES ('2020-01-01', 'd2', 'info2'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, device, info) VALUES ('2020-01-01', 'd1', 'info2'); +-- should fail +\set ON_ERROR_STOP 0 +DELETE FROM metrics WHERE time = '2020-01-01'; +ERROR: update or delete on table "_hyper_7_9_chunk" violates foreign key constraint "event_time_device_fkey" on table "event" +DELETE FROM metrics WHERE device = 'd1'; +ERROR: update or delete on table "_hyper_7_9_chunk" violates foreign key constraint "event_time_device_fkey" on table "event" +\set ON_ERROR_STOP 1 +DROP TABLE event; +DROP TABLE metrics; +-- test single column fk constraint from plain table to hypertable with constraint being added separately while data is present +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0); +CREATE TABLE event(time timestamptz, info text); +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +INSERT INTO event(time, info) VALUES ('2020-02-01', 'info1'); +-- should fail +\set ON_ERROR_STOP 0 +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time) ON DELETE SET NULL; +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +INSERT INTO metrics(time, device, value) VALUES ('2020-02-01', 'd1', 1.0); +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time) ON DELETE CASCADE; +-- delete should cascade +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event; + time | info +------------------------------+------- + Sat Feb 01 00:00:00 2020 PST | info1 +(1 row) + +DROP TABLE event; +DROP TABLE metrics; +-- test single column fk constraint from plain table to compressed hypertable +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); +ALTER TABLE metrics SET(timescaledb.compress, timescaledb.compress_segmentby='device'); +NOTICE: default order by for hypertable "metrics" is set to ""time" DESC" +SELECT count(compress_chunk(ch)) FROM show_chunks('metrics') ch; + count +------- + 1 +(1 row) + +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE CASCADE ON UPDATE CASCADE, info text); +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +ERROR: insert or update on table "event" violates foreign key constraint "event_time_fkey" +\set ON_ERROR_STOP 1 +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; +-- should cascade +BEGIN; +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; + time | info +------+------ +(0 rows) + +ROLLBACK; +-- should cascade +BEGIN; +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; + time | info +------------------------------+------- + Wed Jan 01 00:30:00 2020 PST | info2 +(1 row) + +ROLLBACK; +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; + conname | conrelid | confrelid | parent +------------------+----------+-----------------------------------------+-------- + event_time_fkey | event | metrics | f + event_time_fkey1 | event | _timescaledb_internal._hyper_9_12_chunk | t +(2 rows) + +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgfoid | parent | tgisinternal | tgconstrrelid +-----------------------+--------+--------------+--------------- + "RI_FKey_cascade_del" | f | t | event + "RI_FKey_cascade_upd" | f | t | event + "RI_FKey_cascade_del" | t | t | event + "RI_FKey_cascade_upd" | t | t | event +(4 rows) + +DROP TABLE event; +DROP TABLE metrics; +-- test single column fk constraint from hypertable to hypertable +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + table_name +------------ + metrics +(1 row) + +CREATE TABLE event(time timestamptz, info text); +SELECT table_name FROM create_hypertable('event', 'time'); +NOTICE: adding not-null constraint to column "time" + table_name +------------ + event +(1 row) + +\set ON_ERROR_STOP 0 +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time); +ERROR: hypertables cannot be used as foreign key references of hypertables +\set ON_ERROR_STOP 1 +CREATE TABLE event2(time timestamptz REFERENCES metrics(time), info text); +\set ON_ERROR_STOP 0 +SELECT table_name FROM create_hypertable('event2', 'time'); +ERROR: hypertables cannot be used as foreign key references of hypertables +\set ON_ERROR_STOP 1 +DROP TABLE event; +DROP TABLE event2; +DROP TABLE metrics; diff --git a/tsl/test/sql/CMakeLists.txt b/tsl/test/sql/CMakeLists.txt index 3320af7fddd..2841c98ff10 100644 --- a/tsl/test/sql/CMakeLists.txt +++ b/tsl/test/sql/CMakeLists.txt @@ -31,6 +31,7 @@ set(TEST_FILES compression_sorted_merge_distinct.sql compression_sorted_merge_columns.sql decompress_index.sql + foreign_keys.sql move.sql partialize_finalize.sql policy_generalization.sql diff --git a/tsl/test/sql/foreign_keys.sql b/tsl/test/sql/foreign_keys.sql new file mode 100644 index 00000000000..ba760972e8d --- /dev/null +++ b/tsl/test/sql/foreign_keys.sql @@ -0,0 +1,397 @@ +-- This file and its contents are licensed under the Timescale License. +-- Please see the included NOTICE for copyright information and +-- LICENSE-TIMESCALE for a copy of the license. + +-- test single column fk constraint from plain table to hypertable during hypertable creation +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); + +CREATE TABLE event(time timestamptz references metrics(time), info text); + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); + +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; + +\set ON_ERROR_STOP 0 +-- should fail +DELETE FROM metrics WHERE time = '2020-01-01'; +-- should fail +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +-- create new chunk and repeat the test +INSERT INTO metrics(time, device, value) VALUES ('2021-01-01', 'd1', 1.0); + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2021-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2021-01-01', 'info2'); + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +-- chunks referenced in fk constraints must not be dropped or truncated +\set ON_ERROR_STOP 0 +TRUNCATE metrics; +TRUNCATE _timescaledb_internal._hyper_1_1_chunk; +TRUNCATE _timescaledb_internal._hyper_1_2_chunk; +DROP TABLE _timescaledb_internal._hyper_1_1_chunk; +DROP TABLE _timescaledb_internal._hyper_1_2_chunk; +SELECT drop_chunks('metrics', '1 month'::interval); +\set ON_ERROR_STOP 1 + +-- after removing constraint dropping should succeed +ALTER TABLE event DROP CONSTRAINT event_time_fkey; +SELECT drop_chunks('metrics', '1 month'::interval); + +DROP TABLE event; +DROP TABLE metrics; + +-- test single column fk constraint from plain table to hypertable during hypertable creation with RESTRICT +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); + +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE RESTRICT ON UPDATE RESTRICT, info text); + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); + +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; + +\set ON_ERROR_STOP 0 +-- should fail +DELETE FROM metrics WHERE time = '2020-01-01'; +-- should fail +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +-- create new chunk and repeat the test +INSERT INTO metrics(time, device, value) VALUES ('2021-01-01', 'd1', 1.0); + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2021-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2021-01-01', 'info2'); + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +DROP TABLE event; +DROP TABLE metrics; + +-- test single column fk constraint from plain table to hypertable during hypertable creation with CASCADE +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); + +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE CASCADE ON UPDATE CASCADE, info text); + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); + +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; + +-- should cascade +BEGIN; +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; +ROLLBACK; +-- should cascade +BEGIN; +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; +ROLLBACK; + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +DROP TABLE event; +DROP TABLE metrics; + +-- test single column fk constraint from plain table to hypertable during hypertable creation with SET NULL +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); + +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE SET NULL ON UPDATE SET NULL, info text); + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); + +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; + +-- should cascade +BEGIN; +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; +ROLLBACK; +-- should cascade +BEGIN; +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; +ROLLBACK; + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +DROP TABLE event; +DROP TABLE metrics; + +-- test single column fk constraint from plain table to hypertable during hypertable creation with SET DEFAULT +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); + +CREATE TABLE event(time timestamptz default null references metrics(time) ON DELETE SET DEFAULT ON UPDATE SET DEFAULT, info text); + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); + +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; + +-- should cascade +BEGIN; +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; +ROLLBACK; +-- should cascade +BEGIN; +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; +ROLLBACK; + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +DROP TABLE event; +DROP TABLE metrics; + +-- test single column fk constraint from plain table to hypertable with constraint being added separately +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0); + +CREATE TABLE event(time timestamptz, info text); + +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time) ON DELETE RESTRICT; + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); + +-- should fail +\set ON_ERROR_STOP 0 +DELETE FROM metrics WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +DROP TABLE event; +DROP TABLE metrics; + +-- test multi column fk constraint from plain table to hypertable +CREATE TABLE metrics(time timestamptz , device text, value float, primary key (time, device)); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0); + +CREATE TABLE event(time timestamptz, device text, info text); + +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time,device) REFERENCES metrics(time,device) ON DELETE RESTRICT; + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, device, info) VALUES ('2020-01-02', 'd1', 'info1'); +INSERT INTO event(time, device, info) VALUES ('2020-01-01', 'd2', 'info2'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, device, info) VALUES ('2020-01-01', 'd1', 'info2'); + +-- should fail +\set ON_ERROR_STOP 0 +DELETE FROM metrics WHERE time = '2020-01-01'; +DELETE FROM metrics WHERE device = 'd1'; +\set ON_ERROR_STOP 1 + +DROP TABLE event; +DROP TABLE metrics; + +-- test single column fk constraint from plain table to hypertable with constraint being added separately while data is present +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0); + +CREATE TABLE event(time timestamptz, info text); +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); +INSERT INTO event(time, info) VALUES ('2020-02-01', 'info1'); + +-- should fail +\set ON_ERROR_STOP 0 +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time) ON DELETE SET NULL; +\set ON_ERROR_STOP 1 + +INSERT INTO metrics(time, device, value) VALUES ('2020-02-01', 'd1', 1.0); +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time) ON DELETE CASCADE; + +-- delete should cascade +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event; + +DROP TABLE event; +DROP TABLE metrics; + +-- test single column fk constraint from plain table to compressed hypertable +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 0:00:00', 'd1', 1.0); +INSERT INTO metrics(time, device, value) VALUES ('2020-01-01 1:00:00', 'd1', 1.0); + +ALTER TABLE metrics SET(timescaledb.compress, timescaledb.compress_segmentby='device'); + +SELECT count(compress_chunk(ch)) FROM show_chunks('metrics') ch; + +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE CASCADE ON UPDATE CASCADE, info text); + +-- should fail +\set ON_ERROR_STOP 0 +INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1'); +\set ON_ERROR_STOP 1 +-- should succeed +INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2'); + +\set ON_ERROR_STOP 0 +-- should fail +UPDATE event SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +\set ON_ERROR_STOP 1 + +-- should succeed +BEGIN; +UPDATE event SET time = '2020-01-01 01:00:00' WHERE time = '2020-01-01'; +ROLLBACK; + +-- should cascade +BEGIN; +DELETE FROM metrics WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; +ROLLBACK; +-- should cascade +BEGIN; +UPDATE metrics SET time = '2020-01-01 00:30:00' WHERE time = '2020-01-01'; +SELECT * FROM event ORDER BY event; +ROLLBACK; + +SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid; +SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + +DROP TABLE event; +DROP TABLE metrics; + +-- test single column fk constraint from hypertable to hypertable +CREATE TABLE metrics(time timestamptz primary key, device text, value float); +SELECT table_name FROM create_hypertable('metrics', 'time'); + +CREATE TABLE event(time timestamptz, info text); +SELECT table_name FROM create_hypertable('event', 'time'); + +\set ON_ERROR_STOP 0 +ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time); +\set ON_ERROR_STOP 1 + +CREATE TABLE event2(time timestamptz REFERENCES metrics(time), info text); +\set ON_ERROR_STOP 0 +SELECT table_name FROM create_hypertable('event2', 'time'); +\set ON_ERROR_STOP 1 + +DROP TABLE event; +DROP TABLE event2; +DROP TABLE metrics;