From 1756094b22bf81f15ffdfdb5208075b58c45296f Mon Sep 17 00:00:00 2001 From: eileencodes Date: Fri, 16 Mar 2018 13:20:04 -0400 Subject: [PATCH 1/6] Add DatabaseConfig Struct and associated methods Passing around and parsing hashes is easy if you know that it's a two tier config and each key will be named after the environment and each value will be the config for that environment key. This falls apart pretty quickly with three-tier configs. We have no idea what the second tier will be named (we know the first is primary but we don't know the second), we have no easy way of figuring out how deep a hash we have without iterating over it, and we'd have to do this a lot throughout the code since it breaks all of Active Record's assumptions regarding configurations. These methods allow us to pass around objects instead. This will allow us to more easily parse the configs for the rake tasks. Evenually I'd like to replace the Active Record connection management that passes around config hashes to use these methods as well but that's much farther down the road. `walk_configs` takes an environment, specification name, and a config and turns them into DatabaseConfig struct objects so we can ask the configs questions like: ``` db_config.spec_name => animals db_config.env_name => development db_config.config { :adapter => mysql etc } ``` `db_configs` loops through all given configurations and returns an array of DatabaseConfig structs for each config in the yaml file. and lastly `configs_for` takes an environment and either returns the spec name and config if a block is given or returns an array of DatabaseConfig structs just for the given environment. --- activerecord/lib/active_record/core.rb | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index e1a0b2ecf8..b6b4c76569 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -60,6 +60,45 @@ module ActiveRecord @@configurations end + DatabaseConfig = Struct.new(:env_name, :spec_name, :config) # :nodoc + + # Given an env, spec and config creates DatabaseConfig structs with + # each attribute set. + def self.walk_configs(env_name, spec_name, config) # :nodoc: + if config["database"] || env_name == "default" + DatabaseConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |spec_name, sub_config| + walk_configs(env_name, spec_name, sub_config) + end + end + end + + # Walks all the configs passed in and returns an array + # of DatabaseConfig structs for each configuration. + def self.db_configs(configs = configurations) # :nodoc: + configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name, "primary", config) + end + end + + # Collects the configs for the environment passed in. + # + # If a block is given returns the specification name and configuration + # otherwise returns an array of DatabaseConfig structs for the environment. + def self.configs_for(environment, configs = configurations, &blk) # :nodoc: + env_with_configs = db_configs(configs).select do |db_config| + db_config.env_name == environment + end + + if block_given? + env_with_configs.each do |env_with_config| + yield env_with_config.spec_name, env_with_config.config + end + else + env_with_configs + end + end ## # :singleton-method: # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling From d79d6867a23b90e0f9e2670a3300c9a044c84dca Mon Sep 17 00:00:00 2001 From: eileencodes Date: Fri, 16 Mar 2018 13:56:58 -0400 Subject: [PATCH 2/6] Add create/drop/migrate db tasks for each database in the environment If we have a three-tier yaml file like this: ``` development: primary: database: "development" animals: database: "development_animals" migrations_paths: "db/animals_migrate" ``` This will add db create/drop/and migrate tasks for each level of the config under that environment. ``` bin/rails db:drop:primary bin/rails db:drop:animals bin/rails db:create:primary bin/rails db:create:animals bin/rails db:migrate:primary bin/rails db:migrate:animals ``` --- .../lib/active_record/railties/databases.rake | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 662a8bc720..176e617258 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -22,6 +22,14 @@ db_namespace = namespace :db do task all: :load_config do ActiveRecord::Tasks::DatabaseTasks.create_all end + + databases = Rails.application.config.database_configuration + ActiveRecord::Base.configs_for(Rails.env, databases) do |spec_name, config| + desc "Create #{spec_name} database for current environment" + task spec_name do + ActiveRecord::Tasks::DatabaseTasks.create(config) + end + end end desc "Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases." @@ -33,6 +41,14 @@ db_namespace = namespace :db do task all: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.drop_all end + + databases = Rails.application.config.database_configuration + ActiveRecord::Base.configs_for(Rails.env, databases) do |spec_name, config| + desc "Drop #{spec_name} database for current environment" + task spec_name => :check_protected_environments do + ActiveRecord::Tasks::DatabaseTasks.drop(config) + end + end end desc "Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases." @@ -77,6 +93,15 @@ db_namespace = namespace :db do end namespace :migrate do + databases = Rails.application.config.database_configuration + ActiveRecord::Base.configs_for(Rails.env, databases) do |spec_name, config| + desc "Migrate #{spec_name} database for current environment" + task spec_name do + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? From 5eb4488d02fd975ff5c387a8697fc58cca28b9b4 Mon Sep 17 00:00:00 2001 From: eileencodes Date: Fri, 16 Mar 2018 14:04:09 -0400 Subject: [PATCH 3/6] Add ability to create/drop/migrate all dbs for a given env `each_current_configuration` is used by create, drop, and other methods to find the configs for a given environment and returning those to the method calling them. The change here allows for the database commands to operate on all the configs in the environment. Previously we couldn't slice the hashes and iterate over them becasue they could be two tier or could be three tier. By using the database config structs we don't need to care whether we're dealing with a three tier or two tier, we can just parse all the configs based on the environment. This makes it possible for us to run `bin/rails db:create` and it will create all the configs for the dev and test environment ust like it does for a two tier - it creates the db for dev and test. Now `db:create` will create `primary` for dev and test, and `animals` for dev and test if our database.yml looks like: ``` development: primary: etc animals: etc test: primary: etc animals: etc ``` This means that `bin/rails db:create`, `bin/rails db:drop`, and `bin/rails db:migrate` will operate on the dev and test env for both primary and animals ds. --- .../lib/active_record/railties/databases.rake | 7 +++++-- .../lib/active_record/tasks/database_tasks.rb | 12 ++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 176e617258..787c5f7a5c 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -73,8 +73,11 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::Tasks::DatabaseTasks.migrate - db_namespace["_dump"].invoke + ActiveRecord::Base.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Tasks::DatabaseTasks.migrate + db_namespace["_dump"].invoke + end end # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index af1bbc7e93..705a28eef5 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -261,8 +261,8 @@ module ActiveRecord end def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - each_current_configuration(environment) { |configuration, configuration_environment| - load_schema configuration, format, file, configuration_environment + each_current_configuration(environment) { |configuration, spec_name, env| + load_schema configuration, format, file, env } ActiveRecord::Base.establish_connection(environment.to_sym) end @@ -312,10 +312,10 @@ module ActiveRecord environments = [environment] environments << "test" if environment == "development" - ActiveRecord::Base.configurations.slice(*environments).each do |configuration_environment, configuration| - next unless configuration["database"] - - yield configuration, configuration_environment + environments.each do |env| + ActiveRecord::Base.configs_for(env) do |spec_name, configuration| + yield configuration, spec_name, env + end end end From 0f0aa6a275876502e002c054896734d6536ba5cd Mon Sep 17 00:00:00 2001 From: eileencodes Date: Fri, 16 Mar 2018 14:44:20 -0400 Subject: [PATCH 4/6] Update schema/structure dump tasks for multi db Adds the ability to dump the schema or structure files for mulitple databases. Loops through the configs for a given env and sets a filename based on the format, then establishes a connection for that config and dumps into the file. --- .../lib/active_record/railties/databases.rake | 30 ++++++++++++------- .../lib/active_record/tasks/database_tasks.rb | 18 +++++++++-- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 787c5f7a5c..cd0c4616b8 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -274,10 +274,15 @@ db_namespace = namespace :db do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do require "active_record/schema_dumper" - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") - File.open(filename, "w:utf-8") do |file| - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + + ActiveRecord::Base.configs_for(Rails.env) do |spec_name, config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + File.open(filename, "w:utf-8") do |file| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + end end + db_namespace["schema:dump"].reenable end @@ -304,22 +309,25 @@ db_namespace = namespace :db do rm_f filename, verbose: false end end - end namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") - current_config = ActiveRecord::Tasks::DatabaseTasks.current_config - ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) + ActiveRecord::Base.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) + current_config = ActiveRecord::Tasks::DatabaseTasks.current_config + ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) - if ActiveRecord::SchemaMigration.table_exists? - File.open(filename, "a") do |f| - f.puts ActiveRecord::Base.connection.dump_schema_information - f.print "\n" + if ActiveRecord::SchemaMigration.table_exists? + File.open(filename, "a") do |f| + f.puts ActiveRecord::Base.connection.dump_schema_information + f.print "\n" + end end end + db_namespace["structure:dump"].reenable end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 705a28eef5..e2c292ee56 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -252,14 +252,28 @@ module ActiveRecord end def schema_file(format = ActiveRecord::Base.schema_format) + File.join(db_dir, schema_file_type(format)) + end + + def schema_file_type(format = ActiveRecord::Base.schema_format) case format when :ruby - File.join(db_dir, "schema.rb") + "schema.rb" when :sql - File.join(db_dir, "structure.sql") + "structure.sql" end end + def dump_filename(namespace, format = ActiveRecord::Base.schema_format) + filename = if namespace == "primary" + schema_file_type(format) + else + "#{namespace}_#{schema_file_type(format)}" + end + + ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration, spec_name, env| load_schema configuration, format, file, env From bb9e5540c8fd3f48d461362c652cb86c17d9c5f3 Mon Sep 17 00:00:00 2001 From: eileencodes Date: Fri, 16 Mar 2018 15:07:40 -0400 Subject: [PATCH 5/6] Add tests for new rake tasks --- .../test/cases/tasks/database_tasks_test.rb | 129 +++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 21226352ff..38c2c8b2f3 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -221,7 +221,76 @@ module ActiveRecord ENV["RAILS_ENV"] = old_env end - def test_establishes_connection_for_the_given_environment + def test_establishes_connection_for_the_given_environments + ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + + ActiveRecord::Base.expects(:establish_connection).with(:development) + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + + class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "database" => "prod-db" }, "secondary" => { "database" => "secondary-prod-db" } } + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "prod-db") + + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-prod-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end + + def test_creates_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + + def test_creates_test_and_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + ensure + ENV["RAILS_ENV"] = old_env + end + + def test_establishes_connection_for_the_given_environments_config ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true ActiveRecord::Base.expects(:establish_connection).with(:development) @@ -347,6 +416,64 @@ module ActiveRecord end end + class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "database" => "prod-db" }, "secondary" => { "database" => "secondary-prod-db" } } + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_drops_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "prod-db") + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-prod-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + + def test_drops_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + + def test_drops_testand_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + ensure + ENV["RAILS_ENV"] = old_env + end + end + if current_adapter?(:SQLite3Adapter) && !in_memory_db? class DatabaseTasksMigrateTest < ActiveRecord::TestCase self.use_transactional_tests = false From 4e663c1e8d1e643783f157f904b5276e62f65589 Mon Sep 17 00:00:00 2001 From: eileencodes Date: Tue, 20 Mar 2018 13:47:34 -0400 Subject: [PATCH 6/6] Refactor configs_for and friends Moves the configs_for and DatabaseConfig struct into it's own file. I was considering doing this in a future refactoring but our set up forced me to move it now. You see there are `mattr_accessor`'s on the Core module that have default settings. For example the `schema_format` defaults to Ruby. So if I call `configs_for` or any methods in the Core module it will reset the `schema_format` to `:ruby`. By moving it to it's own class we can keep the logic contained and avoid this unfortunate issue. The second change here does a double loop over the yaml files. Bear with me... Our tests dictate that we need to load an environment before our rake tasks because we could have something in an environment that the database.yml depends on. There are side-effects to this and I think there's a deeper bug that needs to be fixed but that's for another issue. The gist of the problem is when I was creating the dynamic rake tasks if the yaml that that rake task is calling evaluates code (like erb) that calls the environment configs the code will blow up because the environment is not loaded yet. To avoid this issue we added a new method that simply loads the yaml and does not evaluate the erb or anything in it. We then use that yaml to create the task name. Inside the task name we can then call `load_config` and load the real config to actually call the code internal to the task. I admit, this is gross, but refactoring can't all be pretty all the time and I'm working hard with `@tenderlove` to refactor much more of this code to get to a better place re connection management and rake tasks. --- activerecord/lib/active_record.rb | 1 + activerecord/lib/active_record/base.rb | 1 + activerecord/lib/active_record/core.rb | 39 ------------ .../active_record/database_configurations.rb | 63 +++++++++++++++++++ .../lib/active_record/railties/databases.rake | 32 +++++----- .../lib/active_record/tasks/database_tasks.rb | 9 ++- .../lib/rails/application/configuration.rb | 12 ++++ 7 files changed, 101 insertions(+), 56 deletions(-) create mode 100644 activerecord/lib/active_record/database_configurations.rb diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d43378c64f..d198466dbf 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -40,6 +40,7 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache + autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cc99401390..7ab9160265 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -290,6 +290,7 @@ module ActiveRecord #:nodoc: extend CollectionCacheKey include Core + include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index b6b4c76569..e1a0b2ecf8 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -60,45 +60,6 @@ module ActiveRecord @@configurations end - DatabaseConfig = Struct.new(:env_name, :spec_name, :config) # :nodoc - - # Given an env, spec and config creates DatabaseConfig structs with - # each attribute set. - def self.walk_configs(env_name, spec_name, config) # :nodoc: - if config["database"] || env_name == "default" - DatabaseConfig.new(env_name, spec_name, config) - else - config.each_pair.map do |spec_name, sub_config| - walk_configs(env_name, spec_name, sub_config) - end - end - end - - # Walks all the configs passed in and returns an array - # of DatabaseConfig structs for each configuration. - def self.db_configs(configs = configurations) # :nodoc: - configs.each_pair.flat_map do |env_name, config| - walk_configs(env_name, "primary", config) - end - end - - # Collects the configs for the environment passed in. - # - # If a block is given returns the specification name and configuration - # otherwise returns an array of DatabaseConfig structs for the environment. - def self.configs_for(environment, configs = configurations, &blk) # :nodoc: - env_with_configs = db_configs(configs).select do |db_config| - db_config.env_name == environment - end - - if block_given? - env_with_configs.each do |env_with_config| - yield env_with_config.spec_name, env_with_config.config - end - else - env_with_configs - end - end ## # :singleton-method: # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb new file mode 100644 index 0000000000..86624a41c9 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module ActiveRecord + module DatabaseConfigurations # :nodoc: + class DatabaseConfig + attr_reader :env_name, :spec_name, :config + + def initialize(env_name, spec_name, config) + @env_name = env_name + @spec_name = spec_name + @config = config + end + end + + # Selects the config for the specified environment and specification name + # + # For example if passed :development, and :animals it will select the database + # under the :development and :animals configuration level + def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: + configs_for(environment, configs).find do |db_config| + db_config.spec_name == specification_name + end + end + + # Collects the configs for the environment passed in. + # + # If a block is given returns the specification name and configuration + # otherwise returns an array of DatabaseConfig structs for the environment. + def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: + env_with_configs = db_configs(configs).select do |db_config| + db_config.env_name == env + end + + if block_given? + env_with_configs.each do |env_with_config| + yield env_with_config.spec_name, env_with_config.config + end + else + env_with_configs + end + end + + # Given an env, spec and config creates DatabaseConfig structs with + # each attribute set. + def self.walk_configs(env_name, spec_name, config) # :nodoc: + if config["database"] || env_name == "default" + DatabaseConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |spec_name, sub_config| + walk_configs(env_name, spec_name, sub_config) + end + end + end + + # Walks all the configs passed in and returns an array + # of DatabaseConfig structs for each configuration. + def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: + configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name, "primary", config) + end + end + end +end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index cd0c4616b8..76dbcafffb 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -23,11 +23,11 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.create_all end - databases = Rails.application.config.database_configuration - ActiveRecord::Base.configs_for(Rails.env, databases) do |spec_name, config| + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Create #{spec_name} database for current environment" - task spec_name do - ActiveRecord::Tasks::DatabaseTasks.create(config) + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) end end end @@ -42,11 +42,11 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.drop_all end - databases = Rails.application.config.database_configuration - ActiveRecord::Base.configs_for(Rails.env, databases) do |spec_name, config| + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Drop #{spec_name} database for current environment" - task spec_name => :check_protected_environments do - ActiveRecord::Tasks::DatabaseTasks.drop(config) + task spec_name => [:load_config, :check_protected_environments] do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) end end end @@ -73,11 +73,11 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::Base.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| ActiveRecord::Base.establish_connection(config) ActiveRecord::Tasks::DatabaseTasks.migrate - db_namespace["_dump"].invoke end + db_namespace["_dump"].invoke end # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false @@ -96,11 +96,11 @@ db_namespace = namespace :db do end namespace :migrate do - databases = Rails.application.config.database_configuration - ActiveRecord::Base.configs_for(Rails.env, databases) do |spec_name, config| + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Migrate #{spec_name} database for current environment" - task spec_name do - ActiveRecord::Base.establish_connection(config) + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end end @@ -275,7 +275,7 @@ db_namespace = namespace :db do task dump: :load_config do require "active_record/schema_dumper" - ActiveRecord::Base.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) File.open(filename, "w:utf-8") do |file| ActiveRecord::Base.establish_connection(config) @@ -314,7 +314,7 @@ db_namespace = namespace :db do namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - ActiveRecord::Base.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| ActiveRecord::Base.establish_connection(config) filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) current_config = ActiveRecord::Tasks::DatabaseTasks.current_config diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index e2c292ee56..5787660148 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -134,6 +134,13 @@ module ActiveRecord end end + def for_each + databases = Rails.application.config.load_database_yaml + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) do |spec_name, _| + yield spec_name + end + end + def create_current(environment = env) each_current_configuration(environment) { |configuration| create configuration @@ -327,7 +334,7 @@ module ActiveRecord environments << "test" if environment == "development" environments.each do |env| - ActiveRecord::Base.configs_for(env) do |spec_name, configuration| + ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| yield configuration, spec_name, env end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 912faed3e4..f80da4b005 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -166,6 +166,18 @@ module Rails end end + # Loads the database YAML without evaluating ERB. People seem to + # write ERB that makes the database configuration depend on + # Rails configuration. But we want Rails configuration (specifically + # `rake` and `rails` tasks) to be generated based on information in + # the database yaml, so we need a method that loads the database + # yaml *without* the context of the Rails application. + def load_database_yaml # :nodoc: + path = paths["config/database"].existent.first + return {} unless path + YAML.load_file(path.to_s) + end + # Loads and returns the entire raw configuration of database from # values stored in config/database.yml. def database_configuration