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..99158a57ddc --- /dev/null +++ b/src/foreign_key.c @@ -0,0 +1,480 @@ +/* + * 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 "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, + int numfkdelsetcols, AttrNumber *confdelsetcols, + char *conname_addition, Oid parentDelTrigger, + Oid 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 *fks = relation_get_referencing_fk(ht->main_table_relid); + + Relation parentRel = table_open(ht->main_table_relid, AccessShareLock); + foreach (lc, fks) + { + HeapTuple tuple = lfirst(lc); + 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]; + int numfkdelsetcols; + AttrNumber confdelsetcols[INDEX_MAX_KEYS]; + + DeconstructFkConstraintRow(tuple, + &numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop, + &numfkdelsetcols, + confdelsetcols); + + Oid parentDelTrigger = InvalidOid; + Oid parentUpdTrigger = InvalidOid; + + constraint_get_trigger(fk->oid, &parentUpdTrigger, &parentDelTrigger); + char *conname_addition = + ChooseForeignKeyConstraintNameAddition(numfks, confkey, ht->main_table_relid); + + clone_constraint_on_chunk(chunk, + parentRel, + fk, + numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop, + numfkdelsetcols, + confdelsetcols, + conname_addition, + parentDelTrigger, + parentUpdTrigger); + } + table_close(parentRel, 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]; + int numfkdelsetcols; + AttrNumber confdelsetcols[INDEX_MAX_KEYS]; + + DeconstructFkConstraintRow(tuple, + &numfks, + conkey, + confkey, + conpfeqop, + conppeqop, + conffeqop, + &numfkdelsetcols, + confdelsetcols); + + Oid parentDelTrigger = InvalidOid; + Oid parentUpdTrigger = InvalidOid; + + constraint_get_trigger(fk->oid, &parentUpdTrigger, &parentDelTrigger); + char *conname_addition = + ChooseForeignKeyConstraintNameAddition(numfks, confkey, ht->main_table_relid); + + 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, + numfkdelsetcols, + confdelsetcols, + conname_addition, + 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, int numfkdelsetcols, AttrNumber *confdelsetcols, + char *conname_addition, 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 */ + AttrMap *attmap = build_attrmap_by_name(RelationGetDescr(pkrel), RelationGetDescr(parentRel)); + for (int i = 0; i < numfks; i++) + mapped_confkey[i] = attmap->attnums[confkey[i] - 1]; + + table_close(pkrel, NoLock); + + 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, + confdelsetcols, + numfkdelsetcols, + 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; + + 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..964d419dd8f 100644 --- a/src/planner/planner.c +++ b/src/planner/planner.c @@ -383,6 +383,8 @@ preprocess_query(Node *node, PreprocessQueryContext *context) Index rti = 1; bool ret; + // elog(WARNING, "preprocess_query: %s", debug_query_string); + foreach (lc, query->rtable) { RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc); @@ -406,6 +408,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/tsl/test/expected/foreign_keys.out b/tsl/test/expected/foreign_keys.out new file mode 100644 index 00000000000..e603e2f7526 --- /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 tgname, tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgname | tgfoid | parent | tgisinternal | tgconstrrelid +------------------------------+------------------------+--------+--------------+--------------- + RI_ConstraintTrigger_a_17137 | "RI_FKey_restrict_del" | f | t | event + RI_ConstraintTrigger_a_17138 | "RI_FKey_noaction_upd" | f | t | event + RI_ConstraintTrigger_a_17142 | "RI_FKey_restrict_del" | t | t | event + RI_ConstraintTrigger_a_17143 | "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 tgname, tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgname | tgfoid | parent | tgisinternal | tgconstrrelid +------------------------------+------------------------+--------+--------------+--------------- + RI_ConstraintTrigger_a_17137 | "RI_FKey_restrict_del" | f | t | event + RI_ConstraintTrigger_a_17138 | "RI_FKey_noaction_upd" | f | t | event + RI_ConstraintTrigger_a_17142 | "RI_FKey_restrict_del" | t | t | event + RI_ConstraintTrigger_a_17143 | "RI_FKey_noaction_upd" | t | t | event + RI_ConstraintTrigger_a_17153 | "RI_FKey_restrict_del" | t | t | event + RI_ConstraintTrigger_a_17154 | "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 tgname, tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid; + tgname | tgfoid | parent | tgisinternal | tgconstrrelid +------------------------------+------------------------+--------+--------------+--------------- + RI_ConstraintTrigger_a_17177 | "RI_FKey_restrict_del" | f | t | event + RI_ConstraintTrigger_a_17178 | "RI_FKey_noaction_upd" | f | t | event + RI_ConstraintTrigger_a_17182 | "RI_FKey_restrict_del" | t | t | event + RI_ConstraintTrigger_a_17183 | "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..7f452d103a4 --- /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 tgname, 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 tgname, 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 tgname, 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