From 1273842751a2e9e2cce659c8479788ce497369d5 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 27 Jan 2024 08:14:02 +0100 Subject: [PATCH] Add support for foreign keys to hypertables Currrently we only allow Hypertables references other tables, with this patch the opposite direction is supported as well and tables can have foreign key references into hypertables. --- coccinelle/namedata.cocci | 7 +- src/CMakeLists.txt | 1 + src/chunk_constraint.c | 4 + src/foreign_key.c | 518 +++++++++++++++++++++++++++++ src/foreign_key.h | 16 + src/planner/planner.c | 14 + src/process_utility.c | 82 +++-- test/expected/constraint.out | 4 +- test/expected/create_table.out | 1 - test/expected/rowsecurity-15.out | 8 +- tsl/test/expected/foreign_keys.out | 114 +++++++ tsl/test/sql/CMakeLists.txt | 1 + tsl/test/sql/foreign_keys.sql | 73 ++++ 13 files changed, 788 insertions(+), 55 deletions(-) create mode 100644 src/foreign_key.c create mode 100644 src/foreign_key.h create mode 100644 tsl/test/expected/foreign_keys.out create mode 100644 tsl/test/sql/foreign_keys.sql 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..817c996371a --- /dev/null +++ b/src/foreign_key.c @@ -0,0 +1,518 @@ +/* + * 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 "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 List *relation_get_referencing_fk(Oid reloid); +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); + +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 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); +} + +void +ts_fk_propagate(Constraint *c, Hypertable *ht) +{ + ListCell *lc1; + ListCell *lc2; + + List *fks = relation_get_referencing_fk(ht->main_table_relid); + List *chunks = ts_chunk_get_by_hypertable_id(ht->fd.id); + + Relation parentRel = table_open(ht->main_table_relid, AccessShareLock); + + foreach (lc1, fks) + { + HeapTuple tuple = lfirst(lc1); + Form_pg_constraint fk = (Form_pg_constraint) GETSTRUCT(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(tuple, + &numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop +#if PG15_GE + , + &numfkdelsetcols, + confdelsetcols +#endif + ); + + Oid parentDelTrigger = InvalidOid; + Oid parentUpdTrigger = InvalidOid; + + constraint_get_trigger(fk->oid, &parentUpdTrigger, &parentDelTrigger); + + foreach (lc2, chunks) + { + Chunk *chunk = lfirst(lc2); + + /* + * We can skip the OSM chunks here because those + * chunks are immutable and therefore don't need + * the update and delete triggers. + */ + if (chunk->fd.osm_chunk) + continue; + + clone_constraint_on_chunk(chunk, + parentRel, + fk, + numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop, +#if PG15_GE + numfkdelsetcols, + confdelsetcols, +#endif + parentDelTrigger, + parentUpdTrigger); + } + } + + table_close(parentRel, AccessShareLock); +} + +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); + + // XXX FIXME allow non-primary as well and check if it's unique + // XXX FIXME bail when index is missing + Oid indexoid = RelationGetPrimaryKeyIndex(pkrel); + + /* 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]; + + 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 function 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. + */ +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; + } + + CreateTrigger(fk_trigger, + NULL, + refRelOid, + relid, + constraintOid, + indexOid, + InvalidOid, + parentDelTrigger, + NULL, + true, + false); + + /* 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; + } + + CreateTrigger(fk_trigger, + NULL, + refRelOid, + relid, + constraintOid, + indexOid, + InvalidOid, + parentUpdTrigger, + NULL, + true, + false); + + /* 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; +} + +/* 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); +} diff --git a/src/foreign_key.h b/src/foreign_key.h new file mode 100644 index 00000000000..3f78f183508 --- /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(Constraint *c, Hypertable *ht); +extern TSDLLEXPORT void ts_chunk_copy_referencing_fk(const Hypertable *ht, const Chunk *chunk); diff --git a/src/planner/planner.c b/src/planner/planner.c index 2ed58cfcaf6..c2e520cf2a1 100644 --- a/src/planner/planner.c +++ b/src/planner/planner.c @@ -406,6 +406,20 @@ preprocess_query(Node *node, PreprocessQueryContext *context) if (ht) { + /* SELECT 1 FROM ONLY "foo"."bar" x WHERE "time" OPERATOR(pg_catalog.=) $1 + * 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->rellockmode == RowShareLock && + list_length(query->jointree->fromlist) == 1 && + query->jointree->quals && strcmp(rte->eref->aliasname, "x") == 0) + { + rte->inh = true; + } + } + /* Mark hypertable RTEs we'd like to expand ourselves */ if (ts_guc_enable_optimizations && ts_guc_enable_constraint_exclusion && !IS_UPDL_CMD(context->rootquery) && query->resultRelation == 0 && diff --git a/src/process_utility.c b/src/process_utility.c index 6855daea337..ed8087ca665 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" @@ -2228,35 +2229,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. */ @@ -2326,9 +2298,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); @@ -3349,10 +3319,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; @@ -3361,7 +3328,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: @@ -3370,15 +3337,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: @@ -3388,7 +3353,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))) { @@ -3856,7 +3821,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) { @@ -3870,6 +3835,35 @@ process_altertable_end_table(Node *parsetree, CollectedCommand *cmd) break; } } + else + { + /* + * 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) + { + Constraint *c = castNode(Constraint, subcmd->def); + if (c->contype == CONSTR_FOREIGN) + { + Oid relid = RangeVarGetRelid(c->pktable, AccessShareLock, true); + ht = ts_hypertable_cache_get_entry(hcache, relid, CACHE_FLAG_MISSING_OK); + if (ht) + ts_fk_propagate(c, ht); + } + } + } + } + } ts_cache_release(hcache); } diff --git a/test/expected/constraint.out b/test/expected/constraint.out index b22baab9360..5c5fd909359 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 @@ -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 ( 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-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/tsl/test/expected/foreign_keys.out b/tsl/test/expected/foreign_keys.out new file mode 100644 index 00000000000..cf692055585 --- /dev/null +++ b/tsl/test/expected/foreign_keys.out @@ -0,0 +1,114 @@ +-- 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', 'd1', 1.0); +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE 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'); +-- should fail +\set ON_ERROR_STOP 0 +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" +\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_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) + +-- 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_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 + "RI_FKey_restrict_del" | t | t | event + "RI_FKey_noaction_upd" | t | t | event +(6 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_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_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 single column fk constraint from hypertable to hypertable +-- test multi-column fk constraint from plain table to hypertable 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..459a6aecaa2 --- /dev/null +++ b/tsl/test/sql/foreign_keys.sql @@ -0,0 +1,73 @@ +-- 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', 'd1', 1.0); + +CREATE TABLE event(time timestamptz references metrics(time) ON DELETE 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'); + +-- 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; + +-- 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 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 single column fk constraint from hypertable to hypertable +-- test multi-column fk constraint from plain table to hypertable