diff --git a/db/migrate/20260627120000_add_translation_cleanup_triggers.rb b/db/migrate/20260627120000_add_translation_cleanup_triggers.rb new file mode 100644 index 000000000..589b1d943 --- /dev/null +++ b/db/migrate/20260627120000_add_translation_cleanup_triggers.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + + +class AddTranslationCleanupTriggers < Code0::ZeroTrack::Database::Migration[1.0] + include Sagittarius::Database::PolymorphicCleanupTrigger + + TRANSLATION_OWNERS = { + data_types: 'DataType', + flow_types: 'FlowType', + flow_type_settings: 'FlowTypeSetting', + function_definitions: 'FunctionDefinition', + module_configuration_definitions: 'ModuleConfigurationDefinition', + parameter_definitions: 'ParameterDefinition', + runtime_flow_types: 'RuntimeFlowType', + runtime_flow_type_settings: 'RuntimeFlowTypeSetting', + runtime_function_definitions: 'RuntimeFunctionDefinition', + runtime_modules: 'RuntimeModule', + runtime_parameter_definitions: 'RuntimeParameterDefinition', + }.freeze + + def up + TRANSLATION_OWNERS.each do |parent_table, parent_class| + create_polymorphic_cleanup_trigger(:translations, parent_table, :owner, parent_class) + end + end + + def down + TRANSLATION_OWNERS.each_key do |parent_table| + drop_polymorphic_cleanup_trigger(:translations, parent_table) + end + end +end diff --git a/db/schema_migrations/20260627120000 b/db/schema_migrations/20260627120000 new file mode 100644 index 000000000..818a56aeb --- /dev/null +++ b/db/schema_migrations/20260627120000 @@ -0,0 +1 @@ +19dfafb7e212541b59fb7a25cb0662fad87c8b2fd9e9e60f92c260fffdd1edbd \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 08a7b47d0..d889e618a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,3 +1,47 @@ +CREATE FUNCTION delete_data_types_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'DataType' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_flow_type_settings_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'FlowTypeSetting' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_flow_types_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'FlowType' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_function_definitions_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'FunctionDefinition' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_module_configuration_definitions_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'ModuleConfigurationDefinition' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_parameter_definitions_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'ParameterDefinition' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_runtime_flow_type_settings_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'RuntimeFlowTypeSetting' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_runtime_flow_types_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'RuntimeFlowType' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_runtime_function_definitions_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'RuntimeFunctionDefinition' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_runtime_modules_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'RuntimeModule' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + +CREATE FUNCTION delete_runtime_parameter_definitions_translations() RETURNS trigger + LANGUAGE plpgsql + AS $$ BEGIN DELETE FROM "translations" WHERE "owner_type" = 'RuntimeParameterDefinition' AND "owner_id" IN (SELECT id FROM old_rows); RETURN NULL; END; $$; + CREATE TABLE active_storage_attachments ( id bigint NOT NULL, name character varying NOT NULL, @@ -1698,6 +1742,28 @@ CREATE UNIQUE INDEX "index_users_on_LOWER_username" ON users USING btree (lower( CREATE UNIQUE INDEX index_users_on_totp_secret ON users USING btree (totp_secret) WHERE (totp_secret IS NOT NULL); +CREATE TRIGGER trigger_delete_data_types_translations AFTER DELETE ON data_types REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_data_types_translations(); + +CREATE TRIGGER trigger_delete_flow_type_settings_translations AFTER DELETE ON flow_type_settings REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_flow_type_settings_translations(); + +CREATE TRIGGER trigger_delete_flow_types_translations AFTER DELETE ON flow_types REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_flow_types_translations(); + +CREATE TRIGGER trigger_delete_function_definitions_translations AFTER DELETE ON function_definitions REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_function_definitions_translations(); + +CREATE TRIGGER trigger_delete_module_configuration_definitions_translations AFTER DELETE ON module_configuration_definitions REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_module_configuration_definitions_translations(); + +CREATE TRIGGER trigger_delete_parameter_definitions_translations AFTER DELETE ON parameter_definitions REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_parameter_definitions_translations(); + +CREATE TRIGGER trigger_delete_runtime_flow_type_settings_translations AFTER DELETE ON runtime_flow_type_settings REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_runtime_flow_type_settings_translations(); + +CREATE TRIGGER trigger_delete_runtime_flow_types_translations AFTER DELETE ON runtime_flow_types REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_runtime_flow_types_translations(); + +CREATE TRIGGER trigger_delete_runtime_function_definitions_translations AFTER DELETE ON runtime_function_definitions REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_runtime_function_definitions_translations(); + +CREATE TRIGGER trigger_delete_runtime_modules_translations AFTER DELETE ON runtime_modules REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_runtime_modules_translations(); + +CREATE TRIGGER trigger_delete_runtime_parameter_definitions_translations AFTER DELETE ON runtime_parameter_definitions REFERENCING OLD TABLE AS old_rows FOR EACH STATEMENT EXECUTE FUNCTION delete_runtime_parameter_definitions_translations(); + ALTER TABLE ONLY node_parameters ADD CONSTRAINT fk_rails_0d79310cfa FOREIGN KEY (node_function_id) REFERENCES node_functions(id) ON DELETE CASCADE; diff --git a/lib/sagittarius/database/polymorphic_cleanup_trigger.rb b/lib/sagittarius/database/polymorphic_cleanup_trigger.rb new file mode 100644 index 000000000..32c69e6ae --- /dev/null +++ b/lib/sagittarius/database/polymorphic_cleanup_trigger.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Sagittarius + module Database + module PolymorphicCleanupTrigger + def create_polymorphic_cleanup_trigger(table, parent_table, parent_column, parent_class) + function_name = "delete_#{parent_table}_#{table}" + trigger_name = "trigger_#{function_name}" + + execute <<~SQL.squish + CREATE OR REPLACE FUNCTION #{quote_column_name(function_name)}() + RETURNS TRIGGER AS $$ + BEGIN + DELETE FROM #{quote_table_name(table)} + WHERE #{quote_column_name("#{parent_column}_type")} = #{quote(parent_class)} + AND #{quote_column_name("#{parent_column}_id")} IN (SELECT id FROM old_rows); + RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER #{quote_column_name(trigger_name)} + AFTER DELETE ON #{quote_table_name(parent_table)} + REFERENCING OLD TABLE AS old_rows + FOR EACH STATEMENT + EXECUTE FUNCTION #{quote_column_name(function_name)}(); + SQL + end + + def drop_polymorphic_cleanup_trigger(table, parent_table) + function_name = "delete_#{parent_table}_#{table}" + trigger_name = "trigger_#{function_name}" + + execute <<~SQL.squish + DROP TRIGGER #{quote_column_name(trigger_name)} ON #{quote_table_name(parent_table)}; + DROP FUNCTION #{quote_column_name(function_name)}(); + SQL + end + + private + + def quote(value) + connection.quote(value) + end + + def quote_column_name(name) + connection.quote_column_name(name) + end + + def quote_table_name(name) + connection.quote_table_name(name) + end + end + end +end diff --git a/spec/models/translation_spec.rb b/spec/models/translation_spec.rb index b50da044e..dbcf26051 100644 --- a/spec/models/translation_spec.rb +++ b/spec/models/translation_spec.rb @@ -14,6 +14,17 @@ it { is_expected.to validate_presence_of(:content) } end + describe 'polymorphic owner cleanup trigger' do + it 'deletes translations when an owner is deleted directly in PostgreSQL' do + owner = create(:data_type) + translation = create(:translation, owner: owner) + + DataType.where(id: owner.id).delete_all + + expect(described_class).not_to exist(translation.id) + end + end + describe '#to_grpc' do it 'matches the model' do grpc_object = translation.to_grpc