From b97dc07bb0ef43aaf0d8127d85378d40c62c9c91 Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Fri, 5 May 2023 10:07:58 -0400 Subject: [PATCH] feat(sql): Add history tables for host catalogs This adds history tables for `static_host_catalog` and `host_plugin_catalog`. This also adds a base table for the host catalog history tables and an immutable table with one row for when a target has a direct address assigned to it. This also adds a reusable trigger function for tables that are immutable. --- .../oss/postgres/70/04_history_domain.up.sql | 14 ++ .../70/10_host_catalog_history.up.sql | 129 ++++++++++++++++++ internal/db/sqltest/Makefile | 1 + .../db/sqltest/initdb.d/01_colors_persona.sql | 16 +++ .../sqltest/tests/domain/immutable_table.sql | 32 +++++ .../sqltest/tests/history/host_catalogs.sql | 36 +++++ 6 files changed, 228 insertions(+) create mode 100644 internal/db/schema/migrations/oss/postgres/70/10_host_catalog_history.up.sql create mode 100644 internal/db/sqltest/tests/domain/immutable_table.sql create mode 100644 internal/db/sqltest/tests/history/host_catalogs.sql diff --git a/internal/db/schema/migrations/oss/postgres/70/04_history_domain.up.sql b/internal/db/schema/migrations/oss/postgres/70/04_history_domain.up.sql index f0baa9e6db..581bbc6ebd 100644 --- a/internal/db/schema/migrations/oss/postgres/70/04_history_domain.up.sql +++ b/internal/db/schema/migrations/oss/postgres/70/04_history_domain.up.sql @@ -45,4 +45,18 @@ begin; comment on function wt_url_safe_id is 'Returns a random ID of 14 characters containing URL safe characters only'; + create function immutable_table() returns trigger + as $$ + begin + raise exception 'immutable table: %', tg_table_name using + errcode = '23603', + schema = tg_table_schema, + table = tg_table_name; + return null; + end; + $$ language plpgsql; + comment on function immutable_table is + 'immutable_table() is a trigger function that prevents all changes to a table. ' + 'It must be added as a trigger to a table before insert, update, and delete events.'; + commit; diff --git a/internal/db/schema/migrations/oss/postgres/70/10_host_catalog_history.up.sql b/internal/db/schema/migrations/oss/postgres/70/10_host_catalog_history.up.sql new file mode 100644 index 0000000000..d57cd21b49 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/70/10_host_catalog_history.up.sql @@ -0,0 +1,129 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: MPL-2.0 + +begin; + + create table host_catalog_history_base ( + history_id wt_url_safe_id primary key + ); + comment on table host_catalog_history_base is + 'host_catalog_history_base is a base history table ' + 'for host catalog history tables.'; + + create function insert_host_catalog_history_subtype() returns trigger + as $$ + begin + insert into host_catalog_history_base + (history_id) + values + (new.history_id); + return new; + end; + $$ language plpgsql; + comment on function insert_host_catalog_history_subtype is + 'insert_host_catalog_history_subtype is a before insert trigger ' + 'function for subtypes of host_catalog_history_base.'; + + create function delete_host_catalog_history_subtype() returns trigger + as $$ + begin + delete + from host_catalog_history_base + where history_id = old.history_id; + return null; -- result is ignored since this is an after trigger + end; + $$ language plpgsql; + comment on function delete_host_catalog_history_subtype is + 'delete_host_catalog_history_subtype() is an after delete trigger ' + 'function for subtypes of host_catalog_history_base.'; + + create table no_host_catalog_history ( + history_id wt_url_safe_id primary key + constraint host_catalog_history_base_fkey + references host_catalog_history_base (history_id) + on delete restrict + on update restrict + ); + comment on table no_host_catalog_history is + 'no_host_catalog_history is a table with one row to represent ' + 'the case of a target with a direct address associated to it.'; + + insert into host_catalog_history_base values ('_______none'); + insert into no_host_catalog_history values ('_______none'); + + create trigger immutable_table before insert or update or delete on no_host_catalog_history + for each row execute procedure immutable_table(); + + create table static_host_catalog_hst ( + public_id wt_public_id not null, + name text null, + description text null, + project_id wt_scope_id not null, + history_id wt_url_safe_id default wt_url_safe_id() primary key + constraint host_catalog_history_base_fkey + references host_catalog_history_base (history_id) + on delete cascade + on update cascade, + valid_range tstzrange not null default tstzrange(current_timestamp, null), + constraint static_host_catalog_hst_valid_range_excl + exclude using gist (public_id with =, valid_range with &&) + ); + comment on table static_host_catalog_hst is + 'static_host_catalog_hst is a history table where each row contains the values from a row ' + 'in the static_host_catalog table during the time range in the valid_range column.'; + + create trigger insert_host_catalog_history_subtype before insert on static_host_catalog_hst + for each row execute function insert_host_catalog_history_subtype(); + create trigger delete_host_catalog_history_subtype after delete on static_host_catalog_hst + for each row execute function delete_host_catalog_history_subtype(); + + create trigger hst_on_insert after insert on static_host_catalog + for each row execute function hst_on_insert(); + create trigger hst_on_update after update on static_host_catalog + for each row execute function hst_on_update(); + create trigger hst_on_delete after delete on static_host_catalog + for each row execute function hst_on_delete(); + + insert into static_host_catalog_hst + (public_id, name, description, project_id) + select public_id, name, description, project_id + from static_host_catalog; + + create table host_plugin_catalog_hst ( + public_id wt_public_id not null, + name wt_name null, + description text null, + project_id wt_scope_id not null, + plugin_id wt_plugin_id not null, + attributes bytea not null, + history_id wt_url_safe_id default wt_url_safe_id() primary key + constraint host_catalog_history_base_fkey + references host_catalog_history_base (history_id) + on delete cascade + on update cascade, + valid_range tstzrange not null default tstzrange(current_timestamp, null), + constraint host_plugin_catalog_hst_valid_range_excl + exclude using gist (public_id with =, valid_range with &&) + ); + comment on table host_plugin_catalog_hst is + 'host_plugin_catalog_hst is a history table where each row contains the values from a row ' + 'in the host_plugin_catalog table during the time range in the valid_range column.'; + + create trigger insert_host_catalog_history_subtype before insert on host_plugin_catalog_hst + for each row execute function insert_host_catalog_history_subtype(); + create trigger delete_host_catalog_history_subtype after delete on host_plugin_catalog_hst + for each row execute function delete_host_catalog_history_subtype(); + + create trigger hst_on_insert after insert on host_plugin_catalog + for each row execute function hst_on_insert(); + create trigger hst_on_update after update on host_plugin_catalog + for each row execute function hst_on_update(); + create trigger hst_on_delete after delete on host_plugin_catalog + for each row execute function hst_on_delete(); + + insert into host_plugin_catalog_hst + (public_id, name, description, project_id, plugin_id, attributes) + select public_id, name, description, project_id, plugin_id, attributes + from host_plugin_catalog; + +commit; diff --git a/internal/db/sqltest/Makefile b/internal/db/sqltest/Makefile index 7b86092d2d..70d5139883 100644 --- a/internal/db/sqltest/Makefile +++ b/internal/db/sqltest/Makefile @@ -29,6 +29,7 @@ TESTS ?= tests/setup/*.sql \ tests/hcp/*/*.sql \ tests/kms/*.sql \ tests/storage/*.sql \ + tests/domain/*.sql \ tests/history/*.sql \ tests/recording/*.sql diff --git a/internal/db/sqltest/initdb.d/01_colors_persona.sql b/internal/db/sqltest/initdb.d/01_colors_persona.sql index 50f51c40b9..0fb1fe3a76 100644 --- a/internal/db/sqltest/initdb.d/01_colors_persona.sql +++ b/internal/db/sqltest/initdb.d/01_colors_persona.sql @@ -279,6 +279,22 @@ begin; static_host_set as s where h.catalog_id = s.catalog_id; + insert into plugin + (scope_id, public_id, name) + values + ('global', 'plg_____chost', 'Colors Host Plugin'); + + insert into plugin_host_supported + (public_id) + values + ('plg_____chost'); + + insert into host_plugin_catalog + (project_id, plugin_id, public_id, name, attributes) + values + ('p____bcolors', 'plg_____chost', 'c___cb-plghcl', 'Blue Color Plugin Catalog', ''), + ('p____rcolors', 'plg_____chost', 'c___cr-plghcl', 'Red Color Plugin Catalog', ''); + insert into target_tcp (project_id, public_id, name) values diff --git a/internal/db/sqltest/tests/domain/immutable_table.sql b/internal/db/sqltest/tests/domain/immutable_table.sql new file mode 100644 index 0000000000..ec1353a77f --- /dev/null +++ b/internal/db/sqltest/tests/domain/immutable_table.sql @@ -0,0 +1,32 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: MPL-2.0 + +begin; + select plan(5); + + create table test_t1 ( + id text + ); + insert into test_t1 (id) values ('one'); + + create trigger immutable_table before insert or update or delete + on test_t1 for each row execute procedure immutable_table(); + + select is(count(*), 1::bigint) from test_t1; + + prepare insert_fail as insert into test_t1 (id) values ('two'); + select throws_ok('insert_fail', '23603'); + + prepare update_fail as + update test_t1 + set id = 'two' + where id = 'one'; + select throws_ok('update_fail', '23603'); + + prepare delete_fail as delete from test_t1; + select throws_ok('delete_fail', '23603'); + + select is(count(*), 1::bigint) from test_t1; + + select * from finish(); +rollback; diff --git a/internal/db/sqltest/tests/history/host_catalogs.sql b/internal/db/sqltest/tests/history/host_catalogs.sql new file mode 100644 index 0000000000..cbb2d4b2ee --- /dev/null +++ b/internal/db/sqltest/tests/history/host_catalogs.sql @@ -0,0 +1,36 @@ +-- Copyright (c) HashiCorp, Inc. +-- SPDX-License-Identifier: MPL-2.0 + +begin; + select plan(15); + + -- Verify the trigger functions exist and are declared properly + select has_function('insert_host_catalog_history_subtype'); + select volatility_is('insert_host_catalog_history_subtype', 'volatile'); + select isnt_strict('insert_host_catalog_history_subtype'); + + select has_function('delete_host_catalog_history_subtype'); + select volatility_is('delete_host_catalog_history_subtype', 'volatile'); + select isnt_strict('delete_host_catalog_history_subtype'); + + select has_trigger('static_host_catalog_hst', 'insert_host_catalog_history_subtype'); + select has_trigger('static_host_catalog_hst', 'delete_host_catalog_history_subtype'); + select fk_ok('static_host_catalog_hst', 'history_id', 'host_catalog_history_base' , 'history_id'); + + select has_trigger('host_plugin_catalog_hst', 'insert_host_catalog_history_subtype'); + select has_trigger('host_plugin_catalog_hst', 'delete_host_catalog_history_subtype'); + select fk_ok('host_plugin_catalog_hst', 'history_id', 'host_catalog_history_base' , 'history_id'); + + select is(count(*), 1::bigint) from no_host_catalog_history; + select fk_ok('no_host_catalog_history', 'history_id', 'host_catalog_history_base' , 'history_id'); + + select results_eq( + 'select ' + '(select count(*) from static_host_catalog_hst) + ' + '(select count(*) from host_plugin_catalog_hst) + ' + '(select count(*) from no_host_catalog_history)', + 'select count(*) from host_catalog_history_base' + ); + + select * from finish(); +rollback;