From 3f123cbf13b082cdfbd0016bb547416a28b5c3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasir=20Guzm=C3=A1n?= Date: Mon, 23 Feb 2026 22:55:24 -0500 Subject: [PATCH 1/5] Add Instrumentation to send notifications on Rollbacks --- README.md | 44 ++++++++ lib/actual_db_schema.rb | 3 + lib/actual_db_schema/configuration.rb | 6 +- lib/actual_db_schema/engine.rb | 19 +++- lib/actual_db_schema/instrumentation.rb | 7 ++ .../patches/migration_context.rb | 20 +++- .../rollback_stats_repository.rb | 101 ++++++++++++++++++ .../rollback_stats_subscriber.rb | 38 +++++++ .../templates/actual_db_schema.rb | 4 + test/rake_task_db_storage_full_test.rb | 40 +++++++ test/rake_task_test.rb | 40 +++++++ test/rollback_stats_subscriber_test.rb | 92 ++++++++++++++++ test/test_database_filtering.rb | 20 ++++ 13 files changed, 425 insertions(+), 9 deletions(-) create mode 100644 lib/actual_db_schema/instrumentation.rb create mode 100644 lib/actual_db_schema/rollback_stats_repository.rb create mode 100644 lib/actual_db_schema/rollback_stats_subscriber.rb create mode 100644 test/rollback_stats_subscriber_test.rb diff --git a/README.md b/README.md index 6e3f3c40..48f4d86f 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,50 @@ Add the following line to your initializer file (`config/initializers/actual_db_ config.auto_rollback_disabled = true ``` +## Rollback Instrumentation and Stats Subscriber + +ActualDbSchema emits an `ActiveSupport::Notifications` event for each successful phantom rollback: + +- Event name: `rollback.actual_db_schema` +- Event is always emitted when a phantom rollback succeeds +- Stats subscriber is optional and disabled by default + +### Event payload + +| Field | Description | +|-------|-------------| +| `version` | Migration version that was rolled back | +| `name` | Migration class name | +| `database` | Current database name from Active Record config | +| `schema` | Tenant schema name (or `nil` for default schema) | +| `branch` | Branch associated with the migration metadata | +| `manual_mode` | Whether rollback was run in manual mode | +| `storage` | Current migrations storage strategy (`:file` or `:db`) | + +### Enable the built-in stats subscriber + +By default, the stats subscriber is disabled. To enable it, configure it in your initializer: + +```ruby +# config/initializers/actual_db_schema.rb +ActualDbSchema.configure do |config| + config.rollback_stats_subscriber_enabled = true +end +``` + +You can also enable it with: + +```sh +export ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED=true +``` + +Read current counters at runtime: + +```ruby +ActualDbSchema::RollbackStatsSubscriber.stats +# => { total: 3, by_database: { "primary" => 3 }, by_schema: { "default" => 3 }, by_branch: { "main" => 3 } } +``` + ## Automatic Phantom Migration Rollback On Branch Switch By default, the automatic rollback of migrations on branch switch is disabled. If you prefer to automatically rollback phantom migrations whenever you switch branches with `git checkout`, you can enable it in two ways: diff --git a/lib/actual_db_schema.rb b/lib/actual_db_schema.rb index 2ff99d44..c47bd4dc 100644 --- a/lib/actual_db_schema.rb +++ b/lib/actual_db_schema.rb @@ -4,11 +4,14 @@ require "active_record/migration" require "csv" require_relative "actual_db_schema/git" +require_relative "actual_db_schema/rollback_stats_repository" require_relative "actual_db_schema/configuration" +require_relative "actual_db_schema/instrumentation" require_relative "actual_db_schema/store" require_relative "actual_db_schema/version" require_relative "actual_db_schema/migration" require_relative "actual_db_schema/failed_migration" +require_relative "actual_db_schema/rollback_stats_subscriber" require_relative "actual_db_schema/migration_context" require_relative "actual_db_schema/migration_parser" require_relative "actual_db_schema/output_formatter" diff --git a/lib/actual_db_schema/configuration.rb b/lib/actual_db_schema/configuration.rb index f4e736d2..f79e0f43 100644 --- a/lib/actual_db_schema/configuration.rb +++ b/lib/actual_db_schema/configuration.rb @@ -4,7 +4,8 @@ module ActualDbSchema # Manages the configuration settings for the gem. class Configuration attr_accessor :enabled, :auto_rollback_disabled, :ui_enabled, :git_hooks_enabled, :multi_tenant_schemas, - :console_migrations_enabled, :migrated_folder, :migrations_storage, :excluded_databases + :console_migrations_enabled, :migrated_folder, :migrations_storage, :excluded_databases, + :rollback_stats_subscriber_enabled def initialize apply_defaults(default_settings) @@ -41,7 +42,8 @@ def default_settings console_migrations_enabled: env_enabled?("ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"), migrated_folder: ENV["ACTUAL_DB_SCHEMA_MIGRATED_FOLDER"].present?, migrations_storage: migrations_storage_from_env, - excluded_databases: parse_excluded_databases_env + excluded_databases: parse_excluded_databases_env, + rollback_stats_subscriber_enabled: env_enabled?("ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED") } end diff --git a/lib/actual_db_schema/engine.rb b/lib/actual_db_schema/engine.rb index d51e7f88..c87926e3 100644 --- a/lib/actual_db_schema/engine.rb +++ b/lib/actual_db_schema/engine.rb @@ -15,17 +15,28 @@ class Engine < ::Rails::Engine initializer "actual_db_schema.schema_dump_exclusions" do ActiveSupport.on_load(:active_record) do - apply_schema_dump_exclusions + ActualDbSchema::Engine.apply_schema_dump_exclusions end end + initializer "actual_db_schema.rollback_stats_subscriber" do + ActualDbSchema::Engine.setup_rollback_stats_subscriber + end + + def self.setup_rollback_stats_subscriber + return unless ActualDbSchema.config[:rollback_stats_subscriber_enabled] + + ActualDbSchema::RollbackStatsSubscriber.enable! + end + def self.apply_schema_dump_exclusions - table_name = ActualDbSchema::Store::DbAdapter::TABLE_NAME - ignore_schema_dump_table(table_name) + ignore_schema_dump_table(ActualDbSchema::Store::DbAdapter::TABLE_NAME) + ignore_schema_dump_table(ActualDbSchema::RollbackStatsRepository::TABLE_NAME) return unless schema_dump_flags_supported? return unless schema_dump_connection_available? - apply_structure_dump_flags(table_name) + apply_structure_dump_flags(ActualDbSchema::Store::DbAdapter::TABLE_NAME) + apply_structure_dump_flags(ActualDbSchema::RollbackStatsRepository::TABLE_NAME) end class << self diff --git a/lib/actual_db_schema/instrumentation.rb b/lib/actual_db_schema/instrumentation.rb new file mode 100644 index 00000000..2ec8df8b --- /dev/null +++ b/lib/actual_db_schema/instrumentation.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ActualDbSchema + module Instrumentation + ROLLBACK_EVENT = "rollback.actual_db_schema" + end +end diff --git a/lib/actual_db_schema/patches/migration_context.rb b/lib/actual_db_schema/patches/migration_context.rb index 627ae6a2..53a241d9 100644 --- a/lib/actual_db_schema/patches/migration_context.rb +++ b/lib/actual_db_schema/patches/migration_context.rb @@ -37,7 +37,7 @@ def rollback_branches_for_schema(manual_mode: false, schema_name: nil, rolled_ba next unless status_up?(migration) show_info_for(migration, schema_name) if manual_mode - migrate(migration, rolled_back_migrations, schema_name) if !manual_mode || user_wants_rollback? + migrate(migration, rolled_back_migrations, schema_name, manual_mode: manual_mode) if !manual_mode || user_wants_rollback? rescue StandardError => e handle_rollback_error(migration, e, schema_name) end @@ -103,21 +103,35 @@ def show_info_for(migration, schema_name = nil) puts File.read(migration.filename) end - def migrate(migration, rolled_back_migrations, schema_name = nil) + def migrate(migration, rolled_back_migrations, schema_name = nil, manual_mode: false) migration.name = extract_class_name(migration.filename) + branch = branch_for(migration.version.to_s) message = "[ActualDbSchema]" message += " #{schema_name}:" if schema_name message += " Rolling back phantom migration #{migration.version} #{migration.name} " \ - "(from branch: #{branch_for(migration.version.to_s)})" + "(from branch: #{branch})" puts colorize(message, :gray) migrator = down_migrator_for(migration) migrator.extend(ActualDbSchema::Patches::Migrator) migrator.migrate + notify_rollback_migration(migration: migration, schema_name: schema_name, branch: branch, manual_mode: manual_mode) rolled_back_migrations << migration end + def notify_rollback_migration(migration:, schema_name:, branch:, manual_mode:) + ActiveSupport::Notifications.instrument( + ActualDbSchema::Instrumentation::ROLLBACK_EVENT, + version: migration.version.to_s, + name: migration.name, + database: ActualDbSchema.db_config[:database], + schema: schema_name, + branch: branch, + manual_mode: manual_mode + ) + end + def extract_class_name(filename) content = File.read(filename) content.match(/^class\s+([A-Za-z0-9_]+)\s+ Date: Mon, 23 Feb 2026 22:59:56 -0500 Subject: [PATCH 2/5] Fix rubocop offenses --- lib/actual_db_schema/patches/migration_context.rb | 8 ++++++-- lib/actual_db_schema/rollback_stats_repository.rb | 3 ++- lib/actual_db_schema/rollback_stats_subscriber.rb | 8 +++++--- lib/actual_db_schema/schema_diff.rb | 2 +- .../actual_db_schema/templates/actual_db_schema.rb | 2 +- test/rake_task_db_storage_full_test.rb | 12 ++++++------ test/rake_task_test.rb | 12 ++++++------ 7 files changed, 27 insertions(+), 20 deletions(-) diff --git a/lib/actual_db_schema/patches/migration_context.rb b/lib/actual_db_schema/patches/migration_context.rb index 53a241d9..2c445e53 100644 --- a/lib/actual_db_schema/patches/migration_context.rb +++ b/lib/actual_db_schema/patches/migration_context.rb @@ -37,7 +37,10 @@ def rollback_branches_for_schema(manual_mode: false, schema_name: nil, rolled_ba next unless status_up?(migration) show_info_for(migration, schema_name) if manual_mode - migrate(migration, rolled_back_migrations, schema_name, manual_mode: manual_mode) if !manual_mode || user_wants_rollback? + if !manual_mode || user_wants_rollback? + migrate(migration, rolled_back_migrations, schema_name, + manual_mode: manual_mode) + end rescue StandardError => e handle_rollback_error(migration, e, schema_name) end @@ -116,7 +119,8 @@ def migrate(migration, rolled_back_migrations, schema_name = nil, manual_mode: f migrator = down_migrator_for(migration) migrator.extend(ActualDbSchema::Patches::Migrator) migrator.migrate - notify_rollback_migration(migration: migration, schema_name: schema_name, branch: branch, manual_mode: manual_mode) + notify_rollback_migration(migration: migration, schema_name: schema_name, branch: branch, + manual_mode: manual_mode) rolled_back_migrations << migration end diff --git a/lib/actual_db_schema/rollback_stats_repository.rb b/lib/actual_db_schema/rollback_stats_repository.rb index 73e4d54e..6c367feb 100644 --- a/lib/actual_db_schema/rollback_stats_repository.rb +++ b/lib/actual_db_schema/rollback_stats_repository.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true module ActualDbSchema + # Persists rollback events to a DB table so stats survive process restarts. class RollbackStatsRepository TABLE_NAME = "actual_db_schema_rollback_events" class << self - def record(payload) + def record(payload) # rubocop:disable Metrics/AbcSize ensure_table! connection.execute(<<~SQL.squish) INSERT INTO #{quoted_table} diff --git a/lib/actual_db_schema/rollback_stats_subscriber.rb b/lib/actual_db_schema/rollback_stats_subscriber.rb index caf94129..ddd5860e 100644 --- a/lib/actual_db_schema/rollback_stats_subscriber.rb +++ b/lib/actual_db_schema/rollback_stats_subscriber.rb @@ -1,14 +1,16 @@ # frozen_string_literal: true module ActualDbSchema + # Subscribes to rollback events and tracks in-memory counters for observability. class RollbackStatsSubscriber class << self def enable! return if enabled? - @subscription = ActiveSupport::Notifications.subscribe(ActualDbSchema::Instrumentation::ROLLBACK_EVENT) do |_name, _start, _finish, _id, payload| - RollbackStatsRepository.record(payload) - end + @subscription = ActiveSupport::Notifications.subscribe(ActualDbSchema::Instrumentation::ROLLBACK_EVENT) \ + do |_name, _start, _finish, _id, payload| + RollbackStatsRepository.record(payload) + end end def disable! diff --git a/lib/actual_db_schema/schema_diff.rb b/lib/actual_db_schema/schema_diff.rb index f0ab1ed1..44211bc0 100644 --- a/lib/actual_db_schema/schema_diff.rb +++ b/lib/actual_db_schema/schema_diff.rb @@ -207,7 +207,7 @@ def index_action?(action) end def migration_matches?(chg, action, col_or_index_name) - return (chg[:action] == action) if col_or_index_name.nil? + return chg[:action] == action if col_or_index_name.nil? matchers = { rename_column: -> { rename_column_matches?(chg, action, col_or_index_name) }, diff --git a/lib/generators/actual_db_schema/templates/actual_db_schema.rb b/lib/generators/actual_db_schema/templates/actual_db_schema.rb index 57c3351f..cfd5a575 100644 --- a/lib/generators/actual_db_schema/templates/actual_db_schema.rb +++ b/lib/generators/actual_db_schema/templates/actual_db_schema.rb @@ -18,7 +18,7 @@ # Enable automatic phantom migration rollback on branch switch. # config.git_hooks_enabled = true git_hook_enabled_env = ENV["ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"] - config.git_hooks_enabled = git_hook_enabled_env.nil? ? true : git_hook_enabled_env.present? + config.git_hooks_enabled = git_hook_enabled_env.nil? || git_hook_enabled_env.present? # If your application leverages multiple schemas for multi-tenancy, define the active schemas. # config.multi_tenant_schemas = -> { ["public", "tenant1", "tenant2"] } diff --git a/test/rake_task_db_storage_full_test.rb b/test/rake_task_db_storage_full_test.rb index ac2daed8..10811e8a 100644 --- a/test/rake_task_db_storage_full_test.rb +++ b/test/rake_task_db_storage_full_test.rb @@ -63,11 +63,11 @@ def collect_rollback_events end assert_equal 2, events.size - assert_equal %w[20130906111512 20130906111511], events.map { |event| event.payload[:version] } - assert_equal [false, false], events.map { |event| event.payload[:manual_mode] } - assert_equal [utils.primary_database, utils.primary_database], events.map { |event| event.payload[:database] } - assert_equal [nil, nil], events.map { |event| event.payload[:schema] } - assert_equal ["main", "main"], events.map { |event| event.payload[:branch] } + assert_equal(%w[20130906111512 20130906111511], events.map { |event| event.payload[:version] }) + assert_equal([false, false], events.map { |event| event.payload[:manual_mode] }) + assert_equal([utils.primary_database, utils.primary_database], events.map { |event| event.payload[:database] }) + assert_equal([nil, nil], events.map { |event| event.payload[:schema] }) + assert_equal(%w[main main], events.map { |event| event.payload[:branch] }) end describe "with irreversible migration" do @@ -105,7 +105,7 @@ def down events = captured_events end - assert_equal %w[20130906111512 20130906111511], events.map { |event| event.payload[:version] } + assert_equal(%w[20130906111512 20130906111511], events.map { |event| event.payload[:version] }) end end diff --git a/test/rake_task_test.rb b/test/rake_task_test.rb index f63e71b6..405e8c8c 100644 --- a/test/rake_task_test.rb +++ b/test/rake_task_test.rb @@ -61,11 +61,11 @@ def collect_rollback_events end assert_equal 2, events.size - assert_equal %w[20130906111512 20130906111511], events.map { |event| event.payload[:version] } - assert_equal [false, false], events.map { |event| event.payload[:manual_mode] } - assert_equal [utils.primary_database, utils.primary_database], events.map { |event| event.payload[:database] } - assert_equal [nil, nil], events.map { |event| event.payload[:schema] } - assert_equal ["main", "main"], events.map { |event| event.payload[:branch] } + assert_equal(%w[20130906111512 20130906111511], events.map { |event| event.payload[:version] }) + assert_equal([false, false], events.map { |event| event.payload[:manual_mode] }) + assert_equal([utils.primary_database, utils.primary_database], events.map { |event| event.payload[:database] }) + assert_equal([nil, nil], events.map { |event| event.payload[:schema] }) + assert_equal(%w[main main], events.map { |event| event.payload[:branch] }) end describe "with irreversible migration" do @@ -103,7 +103,7 @@ def down events = captured_events end - assert_equal %w[20130906111512 20130906111511], events.map { |event| event.payload[:version] } + assert_equal(%w[20130906111512 20130906111511], events.map { |event| event.payload[:version] }) end end From d648230832ad07257496fe7da6552d7fa465076a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasir=20Guzm=C3=A1n?= Date: Mon, 23 Feb 2026 23:05:58 -0500 Subject: [PATCH 3/5] Fix tests --- test/rake_task_db_storage_full_test.rb | 1 - test/rake_task_test.rb | 1 - 2 files changed, 2 deletions(-) diff --git a/test/rake_task_db_storage_full_test.rb b/test/rake_task_db_storage_full_test.rb index 10811e8a..7123c07c 100644 --- a/test/rake_task_db_storage_full_test.rb +++ b/test/rake_task_db_storage_full_test.rb @@ -67,7 +67,6 @@ def collect_rollback_events assert_equal([false, false], events.map { |event| event.payload[:manual_mode] }) assert_equal([utils.primary_database, utils.primary_database], events.map { |event| event.payload[:database] }) assert_equal([nil, nil], events.map { |event| event.payload[:schema] }) - assert_equal(%w[main main], events.map { |event| event.payload[:branch] }) end describe "with irreversible migration" do diff --git a/test/rake_task_test.rb b/test/rake_task_test.rb index 405e8c8c..553d27b5 100644 --- a/test/rake_task_test.rb +++ b/test/rake_task_test.rb @@ -65,7 +65,6 @@ def collect_rollback_events assert_equal([false, false], events.map { |event| event.payload[:manual_mode] }) assert_equal([utils.primary_database, utils.primary_database], events.map { |event| event.payload[:database] }) assert_equal([nil, nil], events.map { |event| event.payload[:schema] }) - assert_equal(%w[main main], events.map { |event| event.payload[:branch] }) end describe "with irreversible migration" do From 143809a3060d3bf24a7891e809458e8001252bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasir=20Guzm=C3=A1n?= Date: Tue, 24 Feb 2026 09:22:13 -0500 Subject: [PATCH 4/5] Remove RollbackStatsSubscriber --- README.md | 30 +++--- lib/actual_db_schema.rb | 1 - lib/actual_db_schema/configuration.rb | 6 +- lib/actual_db_schema/engine.rb | 10 -- .../rollback_stats_subscriber.rb | 40 -------- .../templates/actual_db_schema.rb | 13 ++- test/rollback_stats_subscriber_test.rb | 92 ------------------- test/test_database_filtering.rb | 20 ---- 8 files changed, 26 insertions(+), 186 deletions(-) delete mode 100644 lib/actual_db_schema/rollback_stats_subscriber.rb delete mode 100644 test/rollback_stats_subscriber_test.rb diff --git a/README.md b/README.md index 48f4d86f..c4afc9e1 100644 --- a/README.md +++ b/README.md @@ -230,13 +230,12 @@ Add the following line to your initializer file (`config/initializers/actual_db_ config.auto_rollback_disabled = true ``` -## Rollback Instrumentation and Stats Subscriber +## Rollback Instrumentation ActualDbSchema emits an `ActiveSupport::Notifications` event for each successful phantom rollback: -- Event name: `rollback.actual_db_schema` +- Event name: `rollback_migration.actual_db_schema` - Event is always emitted when a phantom rollback succeeds -- Stats subscriber is optional and disabled by default ### Event payload @@ -248,30 +247,31 @@ ActualDbSchema emits an `ActiveSupport::Notifications` event for each successful | `schema` | Tenant schema name (or `nil` for default schema) | | `branch` | Branch associated with the migration metadata | | `manual_mode` | Whether rollback was run in manual mode | -| `storage` | Current migrations storage strategy (`:file` or `:db`) | -### Enable the built-in stats subscriber +### Subscribing to rollback events -By default, the stats subscriber is disabled. To enable it, configure it in your initializer: +You can subscribe to rollback events in your initializer to track statistics or perform custom actions: ```ruby # config/initializers/actual_db_schema.rb -ActualDbSchema.configure do |config| - config.rollback_stats_subscriber_enabled = true +ActiveSupport::Notifications.subscribe(ActualDbSchema::Instrumentation::ROLLBACK_EVENT) do |_name, _start, _finish, _id, payload| + ActualDbSchema::RollbackStatsRepository.record(payload) end ``` -You can also enable it with: - -```sh -export ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED=true -``` +The `RollbackStatsRepository` persists rollback events to a database table (`actual_db_schema_rollback_events`) that is automatically excluded from schema dumps. -Read current counters at runtime: +Read aggregated stats at runtime: ```ruby -ActualDbSchema::RollbackStatsSubscriber.stats +ActualDbSchema::RollbackStatsRepository.stats # => { total: 3, by_database: { "primary" => 3 }, by_schema: { "default" => 3 }, by_branch: { "main" => 3 } } + +ActualDbSchema::RollbackStatsRepository.total_rollbacks +# => 3 + +ActualDbSchema::RollbackStatsRepository.reset! +# Clears all recorded stats ``` ## Automatic Phantom Migration Rollback On Branch Switch diff --git a/lib/actual_db_schema.rb b/lib/actual_db_schema.rb index c47bd4dc..a38ea7ed 100644 --- a/lib/actual_db_schema.rb +++ b/lib/actual_db_schema.rb @@ -11,7 +11,6 @@ require_relative "actual_db_schema/version" require_relative "actual_db_schema/migration" require_relative "actual_db_schema/failed_migration" -require_relative "actual_db_schema/rollback_stats_subscriber" require_relative "actual_db_schema/migration_context" require_relative "actual_db_schema/migration_parser" require_relative "actual_db_schema/output_formatter" diff --git a/lib/actual_db_schema/configuration.rb b/lib/actual_db_schema/configuration.rb index f79e0f43..f4e736d2 100644 --- a/lib/actual_db_schema/configuration.rb +++ b/lib/actual_db_schema/configuration.rb @@ -4,8 +4,7 @@ module ActualDbSchema # Manages the configuration settings for the gem. class Configuration attr_accessor :enabled, :auto_rollback_disabled, :ui_enabled, :git_hooks_enabled, :multi_tenant_schemas, - :console_migrations_enabled, :migrated_folder, :migrations_storage, :excluded_databases, - :rollback_stats_subscriber_enabled + :console_migrations_enabled, :migrated_folder, :migrations_storage, :excluded_databases def initialize apply_defaults(default_settings) @@ -42,8 +41,7 @@ def default_settings console_migrations_enabled: env_enabled?("ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"), migrated_folder: ENV["ACTUAL_DB_SCHEMA_MIGRATED_FOLDER"].present?, migrations_storage: migrations_storage_from_env, - excluded_databases: parse_excluded_databases_env, - rollback_stats_subscriber_enabled: env_enabled?("ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED") + excluded_databases: parse_excluded_databases_env } end diff --git a/lib/actual_db_schema/engine.rb b/lib/actual_db_schema/engine.rb index c87926e3..cc1358a3 100644 --- a/lib/actual_db_schema/engine.rb +++ b/lib/actual_db_schema/engine.rb @@ -19,16 +19,6 @@ class Engine < ::Rails::Engine end end - initializer "actual_db_schema.rollback_stats_subscriber" do - ActualDbSchema::Engine.setup_rollback_stats_subscriber - end - - def self.setup_rollback_stats_subscriber - return unless ActualDbSchema.config[:rollback_stats_subscriber_enabled] - - ActualDbSchema::RollbackStatsSubscriber.enable! - end - def self.apply_schema_dump_exclusions ignore_schema_dump_table(ActualDbSchema::Store::DbAdapter::TABLE_NAME) ignore_schema_dump_table(ActualDbSchema::RollbackStatsRepository::TABLE_NAME) diff --git a/lib/actual_db_schema/rollback_stats_subscriber.rb b/lib/actual_db_schema/rollback_stats_subscriber.rb deleted file mode 100644 index ddd5860e..00000000 --- a/lib/actual_db_schema/rollback_stats_subscriber.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module ActualDbSchema - # Subscribes to rollback events and tracks in-memory counters for observability. - class RollbackStatsSubscriber - class << self - def enable! - return if enabled? - - @subscription = ActiveSupport::Notifications.subscribe(ActualDbSchema::Instrumentation::ROLLBACK_EVENT) \ - do |_name, _start, _finish, _id, payload| - RollbackStatsRepository.record(payload) - end - end - - def disable! - return unless enabled? - - ActiveSupport::Notifications.unsubscribe(@subscription) - @subscription = nil - end - - def enabled? - !@subscription.nil? - end - - def stats - RollbackStatsRepository.stats - end - - def total_rollbacks - RollbackStatsRepository.total_rollbacks - end - - def reset! - RollbackStatsRepository.reset! - end - end - end -end diff --git a/lib/generators/actual_db_schema/templates/actual_db_schema.rb b/lib/generators/actual_db_schema/templates/actual_db_schema.rb index cfd5a575..a4006305 100644 --- a/lib/generators/actual_db_schema/templates/actual_db_schema.rb +++ b/lib/generators/actual_db_schema/templates/actual_db_schema.rb @@ -27,10 +27,6 @@ # config.console_migrations_enabled = true config.console_migrations_enabled = ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present? - # This subscribes to rollback events and tracks in-memory counters for observability. - # config.rollback_stats_subscriber_enabled = true - config.rollback_stats_subscriber_enabled = ENV["ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED"].present? - # Define the migrated folder location. # config.migrated_folder = Rails.root.join("custom", "migrated") config.migrated_folder = Rails.root.join("tmp", "migrated") @@ -39,4 +35,13 @@ # config.migrations_storage = :db config.migrations_storage = :file end + + # Subscribe to rollback events to persist stats (optional). + # Uncomment the following to track rollback statistics in the database: + # + # ActiveSupport::Notifications.subscribe( + # ActualDbSchema::Instrumentation::ROLLBACK_EVENT + # ) do |_name, _start, _finish, _id, payload| + # ActualDbSchema::RollbackStatsRepository.record(payload) + # end end diff --git a/test/rollback_stats_subscriber_test.rb b/test/rollback_stats_subscriber_test.rb deleted file mode 100644 index b75d0890..00000000 --- a/test/rollback_stats_subscriber_test.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -describe ActualDbSchema::RollbackStatsSubscriber do - let(:subscriber) { ActualDbSchema::RollbackStatsSubscriber } - - before do - subscriber.disable! - subscriber.reset! - ActualDbSchema.config[:rollback_stats_subscriber_enabled] = false - end - - after do - subscriber.disable! - subscriber.reset! - ActualDbSchema.config[:rollback_stats_subscriber_enabled] = false - end - - it "is disabled by default" do - refute subscriber.enabled? - assert_equal 0, subscriber.total_rollbacks - end - - it "collects rollback stats from instrumentation events" do - subscriber.enable! - - ActiveSupport::Notifications.instrument( - ActualDbSchema::Instrumentation::ROLLBACK_EVENT, - version: "20260101010101", - name: "CreateUsers", - database: "primary", - schema: nil, - branch: "main", - manual_mode: false - ) - - ActiveSupport::Notifications.instrument( - ActualDbSchema::Instrumentation::ROLLBACK_EVENT, - version: "20260101010102", - name: "CreatePosts", - database: "primary", - schema: "tenant_one", - branch: "main", - manual_mode: true - ) - - stats = subscriber.stats - assert_equal 2, stats[:total] - assert_equal 2, subscriber.total_rollbacks - assert_equal 2, stats[:by_database]["primary"] - assert_equal 1, stats[:by_schema]["default"] - assert_equal 1, stats[:by_schema]["tenant_one"] - assert_equal 2, stats[:by_branch]["main"] - end - - it "disables and unsubscribes from events" do - subscriber.enable! - subscriber.disable! - refute subscriber.enabled? - - ActiveSupport::Notifications.instrument( - ActualDbSchema::Instrumentation::ROLLBACK_EVENT, - { version: "20260101010101", database: "primary", schema: nil, branch: "main" } - ) - - assert_equal 0, subscriber.total_rollbacks - end - - it "resets accumulated stats" do - subscriber.enable! - - ActiveSupport::Notifications.instrument( - ActualDbSchema::Instrumentation::ROLLBACK_EVENT, - { version: "20260101010101", database: "primary", schema: nil, branch: "main" } - ) - - assert_equal 1, subscriber.total_rollbacks - subscriber.reset! - assert_equal 0, subscriber.total_rollbacks - assert_equal({}, subscriber.stats[:by_database]) - end - - it "enables subscriber via engine setup when config flag is true" do - refute subscriber.enabled? - - ActualDbSchema.config[:rollback_stats_subscriber_enabled] = true - ActualDbSchema::Engine.setup_rollback_stats_subscriber - - assert subscriber.enabled? - end -end diff --git a/test/test_database_filtering.rb b/test/test_database_filtering.rb index 1f6da1b5..b8e545d6 100644 --- a/test/test_database_filtering.rb +++ b/test/test_database_filtering.rb @@ -149,24 +149,4 @@ def config_name(db_config) ENV.delete("ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES") end end - - describe "environment variable ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED" do - it "enables rollback stats subscriber from environment variable" do - ENV["ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED"] = "true" - - config = ActualDbSchema::Configuration.new - - assert_equal true, config.rollback_stats_subscriber_enabled - ensure - ENV.delete("ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED") - end - - it "is disabled by default when environment variable is not set" do - ENV.delete("ACTUAL_DB_SCHEMA_ROLLBACK_STATS_SUBSCRIBER_ENABLED") - - config = ActualDbSchema::Configuration.new - - assert_equal false, config.rollback_stats_subscriber_enabled - end - end end From 31a16f9da162a9a5a41823967bdcbd45ed948aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasir=20Guzm=C3=A1n?= Date: Tue, 24 Feb 2026 21:15:49 -0500 Subject: [PATCH 5/5] Remove unrelated changes --- .rubocop.yml | 3 +++ lib/actual_db_schema/rollback_stats_repository.rb | 4 ++-- lib/actual_db_schema/schema_diff.rb | 2 +- lib/generators/actual_db_schema/templates/actual_db_schema.rb | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 226316cd..90c64ac7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -40,3 +40,6 @@ Metrics/CyclomaticComplexity: Metrics/PerceivedComplexity: Max: 10 + +Metrics/AbcSize: + Enabled: false diff --git a/lib/actual_db_schema/rollback_stats_repository.rb b/lib/actual_db_schema/rollback_stats_repository.rb index 6c367feb..de5d6a79 100644 --- a/lib/actual_db_schema/rollback_stats_repository.rb +++ b/lib/actual_db_schema/rollback_stats_repository.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module ActualDbSchema - # Persists rollback events to a DB table so stats survive process restarts. + # Persists rollback events in DB. class RollbackStatsRepository TABLE_NAME = "actual_db_schema_rollback_events" class << self - def record(payload) # rubocop:disable Metrics/AbcSize + def record(payload) ensure_table! connection.execute(<<~SQL.squish) INSERT INTO #{quoted_table} diff --git a/lib/actual_db_schema/schema_diff.rb b/lib/actual_db_schema/schema_diff.rb index 44211bc0..f0ab1ed1 100644 --- a/lib/actual_db_schema/schema_diff.rb +++ b/lib/actual_db_schema/schema_diff.rb @@ -207,7 +207,7 @@ def index_action?(action) end def migration_matches?(chg, action, col_or_index_name) - return chg[:action] == action if col_or_index_name.nil? + return (chg[:action] == action) if col_or_index_name.nil? matchers = { rename_column: -> { rename_column_matches?(chg, action, col_or_index_name) }, diff --git a/lib/generators/actual_db_schema/templates/actual_db_schema.rb b/lib/generators/actual_db_schema/templates/actual_db_schema.rb index a4006305..555d3f60 100644 --- a/lib/generators/actual_db_schema/templates/actual_db_schema.rb +++ b/lib/generators/actual_db_schema/templates/actual_db_schema.rb @@ -18,7 +18,7 @@ # Enable automatic phantom migration rollback on branch switch. # config.git_hooks_enabled = true git_hook_enabled_env = ENV["ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"] - config.git_hooks_enabled = git_hook_enabled_env.nil? || git_hook_enabled_env.present? + config.git_hooks_enabled = git_hook_enabled_env.nil? ? true : git_hook_enabled_env.present? # If your application leverages multiple schemas for multi-tenancy, define the active schemas. # config.multi_tenant_schemas = -> { ["public", "tenant1", "tenant2"] }