Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
Expand Down
41 changes: 34 additions & 7 deletions lib/actual_db_schema/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
27 changes: 22 additions & 5 deletions lib/actual_db_schema/migration_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
152 changes: 152 additions & 0 deletions test/test_database_filtering.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand Down