diff --git a/README.md b/README.md index 3b90010..6e3f3c4 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,39 @@ This task will prompt you to choose one of the three options: Based on your selection, a post-checkout hook will be installed or updated in your `.git/hooks` folder. +## Excluding Databases from Processing + +**For Rails 6.1+ applications using multiple databases** (especially with infrastructure databases like Solid Queue, Solid Cable, or Solid Cache), you can exclude specific databases from ActualDbSchema's processing to prevent connection conflicts. + +### Why You Might Need This + +Modern Rails applications often use the `connects_to` pattern for infrastructure databases. These databases maintain their own isolated connection pools, and ActualDbSchema's global connection switching can interfere with active queries. This is particularly common with: + +- **Solid Queue** (Rails 8 default job backend) +- **Solid Cable** (WebSocket connections) +- **Solid Cache** (caching infrastructure) + +### Method 1: Using `excluded_databases` Configuration + +Explicitly exclude databases by name in your initializer: + +```ruby +# config/initializers/actual_db_schema.rb +ActualDbSchema.configure do |config| + config.excluded_databases = [:queue, :cable, :cache] +end +``` + +### Method 2: Using Environment Variable + +Set the environment variable `ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES` with a comma-separated list: + +```sh +export ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES="queue,cable,cache" +``` + +**Note:** If both the environment variable and the configuration setting in the initializer are provided, the configuration setting takes precedence as it's applied after the default settings are loaded. + ## Multi-Tenancy Support If your application leverages multiple schemas for multi-tenancy — such as those implemented by the [apartment](https://github.com/influitive/apartment) gem or similar solutions — you can configure ActualDbSchema to handle migrations across all schemas. To do so, add the following configuration to your initializer file (`config/initializers/actual_db_schema.rb`): diff --git a/lib/actual_db_schema/configuration.rb b/lib/actual_db_schema/configuration.rb index bb0c893..f4e736d 100644 --- a/lib/actual_db_schema/configuration.rb +++ b/lib/actual_db_schema/configuration.rb @@ -4,7 +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 + :console_migrations_enabled, :migrated_folder, :migrations_storage, :excluded_databases def initialize apply_defaults(default_settings) @@ -33,17 +33,44 @@ def fetch(key, default = nil) def default_settings { - enabled: Rails.env.development?, - auto_rollback_disabled: ENV["ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED"].present?, - ui_enabled: Rails.env.development? || ENV["ACTUAL_DB_SCHEMA_UI_ENABLED"].present?, - git_hooks_enabled: ENV["ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"].present?, + enabled: enabled_by_default?, + auto_rollback_disabled: env_enabled?("ACTUAL_DB_SCHEMA_AUTO_ROLLBACK_DISABLED"), + ui_enabled: ui_enabled_by_default?, + git_hooks_enabled: env_enabled?("ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"), multi_tenant_schemas: nil, - console_migrations_enabled: ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present?, + console_migrations_enabled: env_enabled?("ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"), migrated_folder: ENV["ACTUAL_DB_SCHEMA_MIGRATED_FOLDER"].present?, - migrations_storage: ENV.fetch("ACTUAL_DB_SCHEMA_MIGRATIONS_STORAGE", "file").to_sym + migrations_storage: migrations_storage_from_env, + excluded_databases: parse_excluded_databases_env } end + def enabled_by_default? + Rails.env.development? + end + + def ui_enabled_by_default? + Rails.env.development? || env_enabled?("ACTUAL_DB_SCHEMA_UI_ENABLED") + end + + def env_enabled?(key) + ENV[key].present? + end + + def migrations_storage_from_env + ENV.fetch("ACTUAL_DB_SCHEMA_MIGRATIONS_STORAGE", "file").to_sym + end + + def parse_excluded_databases_env + return [] unless ENV["ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES"].present? + + ENV["ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES"] + .split(",") + .map(&:strip) + .reject(&:empty?) + .map(&:to_sym) + end + def apply_defaults(settings) settings.each do |key, value| instance_variable_set("@#{key}", value) diff --git a/lib/actual_db_schema/migration_context.rb b/lib/actual_db_schema/migration_context.rb index 338efb6..462444f 100644 --- a/lib/actual_db_schema/migration_context.rb +++ b/lib/actual_db_schema/migration_context.rb @@ -20,11 +20,28 @@ def establish_connection(db_config) end def configs - # Rails < 6.0 has a Hash in configurations - if ActiveRecord::Base.configurations.is_a?(Hash) - [ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]] - else - ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env) + all_configs = if ActiveRecord::Base.configurations.is_a?(Hash) + # Rails < 6.0 has a Hash in configurations + [ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]] + else + ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env) + end + + filter_configs(all_configs) + end + + def filter_configs(all_configs) + all_configs.reject do |db_config| + # Skip if database is in the excluded list + # Rails 6.0 uses spec_name, Rails 6.1+ uses name + db_name = if db_config.respond_to?(:name) + db_config.name.to_sym + elsif db_config.respond_to?(:spec_name) + db_config.spec_name.to_sym + else + :primary + end + ActualDbSchema.config.excluded_databases.include?(db_name) end end diff --git a/test/test_database_filtering.rb b/test/test_database_filtering.rb new file mode 100644 index 0000000..b8e545d --- /dev/null +++ b/test/test_database_filtering.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require "test_helper" + +describe "database filtering" do + let(:utils) do + TestUtils.new( + migrations_path: ["db/migrate", "db/migrate_secondary"], + migrated_path: ["tmp/migrated", "tmp/migrated_migrate_secondary"] + ) + end + + # Helper to extract config name that works with Rails 6.0 (spec_name) and Rails 6.1+ (name) + def config_name(db_config) + if db_config.respond_to?(:name) + db_config.name.to_sym + elsif db_config.respond_to?(:spec_name) + db_config.spec_name.to_sym + else + :primary + end + end + + before do + # Reset to default config + ActualDbSchema.config.excluded_databases = [] + end + + after do + # Clean up configuration after each test + ActualDbSchema.config.excluded_databases = [] + end + + describe "with excluded_databases configuration" do + it "excludes databases from the excluded_databases list" do + db_config = TestingState.db_config.dup + utils.reset_database_yml(db_config) + ActiveRecord::Base.configurations = { "test" => db_config } + ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => db_config } + + # Configure to exclude secondary database + ActualDbSchema.config.excluded_databases = [:secondary] + + # Get the migration context instance + context = ActualDbSchema::MigrationContext.instance + + # Verify only primary database is included + configs = context.send(:configs) + config_names = configs.map { |c| config_name(c) } + + assert_includes config_names, :primary + refute_includes config_names, :secondary + end + + it "allows excluding multiple databases" do + db_config = { + "primary" => TestingState.db_config["primary"], + "secondary" => TestingState.db_config["secondary"], + "queue" => { + "adapter" => "sqlite3", + "database" => "tmp/queue.sqlite3", + "migrations_paths" => Rails.root.join("db", "migrate_queue").to_s + } + } + + utils.reset_database_yml(db_config) + ActiveRecord::Base.configurations = { "test" => db_config } + ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => db_config } + + # Configure to exclude secondary and queue databases + ActualDbSchema.config.excluded_databases = %i[secondary queue] + + # Get the migration context instance + context = ActualDbSchema::MigrationContext.instance + + # Verify only primary database is included + configs = context.send(:configs) + config_names = configs.map { |c| config_name(c) } + + assert_includes config_names, :primary + refute_includes config_names, :secondary + refute_includes config_names, :queue + end + + it "processes all databases when excluded_databases is empty" do + db_config = TestingState.db_config.dup + utils.reset_database_yml(db_config) + ActiveRecord::Base.configurations = { "test" => db_config } + ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => db_config } + + ActualDbSchema.config.excluded_databases = [] + + context = ActualDbSchema::MigrationContext.instance + configs = context.send(:configs) + config_names = configs.map { |c| config_name(c) } + + assert_includes config_names, :primary + assert_includes config_names, :secondary + end + end + + describe "environment variable ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES" do + it "parses comma-separated database names from environment variable" do + ENV["ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES"] = "queue,cable" + + # Create a new configuration to pick up the env var + config = ActualDbSchema::Configuration.new + + assert_equal %i[queue cable], config.excluded_databases + ensure + ENV.delete("ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES") + end + + it "handles whitespace in environment variable" do + ENV["ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES"] = "queue, cable, cache" + + config = ActualDbSchema::Configuration.new + + assert_equal %i[queue cable cache], config.excluded_databases + ensure + ENV.delete("ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES") + end + + it "returns empty array when environment variable is not set" do + ENV.delete("ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES") + + config = ActualDbSchema::Configuration.new + + assert_equal [], config.excluded_databases + end + + it "handles empty string in environment variable" do + ENV["ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES"] = "" + + config = ActualDbSchema::Configuration.new + + assert_equal [], config.excluded_databases + ensure + ENV.delete("ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES") + end + + it "filters out empty values from comma-separated list" do + ENV["ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES"] = "queue,,cable, ,cache" + + config = ActualDbSchema::Configuration.new + + assert_equal %i[queue cable cache], config.excluded_databases + ensure + ENV.delete("ACTUAL_DB_SCHEMA_EXCLUDED_DATABASES") + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 10dfb23..25d1a45 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true $LOAD_PATH.unshift File.expand_path("../lib", __dir__) + +# Clear DATABASE_URL to prevent it from overriding the test database configuration +ENV.delete("DATABASE_URL") + require "logger" require "rails/all" require "actual_db_schema"